Material UIで複数のテーマをユーザが切り替えられるようにします。
完成図はこちら。リロードしてもテーマは維持されたままになります。

Contents
はじめに
テーマをReactのContextで管理し、ユーザが選択することでテーマを切り替えられるようにします。
また、ユーザがどのテーマを選んだかをLocal Storageに保存しておき、リロード時にテーマが保たれるようにします。
ポイント
本記事ではReact Hooksを使っています。
準備
以下のGitHubのコードを解説していきます。
実装
Material UIテーマの適用
まずはMaterial UIのテーマを適用します。App.jsのreturn文の箇所。
ThemeProviderにtheme={テーマ}でテーマを渡すと全体に適用されます。
return (
<ThemeProvider theme={ここにテーマ}>
<CssBaseline />
<CustomBar />
<Container maxWidth="sm">
<DemoContents />
</Container>
</ThemeProvider>
);
プロジェクトのthemeフォルダにあるdefaultTheme, darkTheme, navyThemeを渡すとそれぞれテーマを適用することが出来ます。

defaultTheme
テーマをContextで管理
次にこのThemeProviderに渡すテーマをReactのContext で管理します。
theme/themeContext.jsは以下のようになります。
import React, { useState } from "react";
import darkTheme from "./darkTheme";
import defaultTheme from "./defaultTheme";
import navyTheme from "./navyTheme";
export const ThemeContext = React.createContext({
// このように設定するとIDEの補完が機能する
theme: null,
handleThemeChange: () => {}
});
const ThemeContextProvider = props => {
const [theme, setTheme] = useState(defaultTheme);
const handleThemeChange = themeName => {
localStorage.setItem("theme", themeName);
switch (themeName) {
case "DEFAULT":
setTheme(defaultTheme);
break;
case "DARK":
setTheme(darkTheme);
break;
case "NAVY":
setTheme(navyTheme);
break;
default:
setTheme(defaultTheme);
break;
}
};
return (
<ThemeContext.Provider
value={{ handleThemeChange: handleThemeChange, theme: theme }}
>
{props.children}
</ThemeContext.Provider>
);
};
export default ThemeContextProvider;ThemeContextとThemeContextProviderを作成しています。
ThemeContextは変数の宣言だけ行います。こうすることでIDEの補完が効くようになります。
ThemeContextProviderではuseStateを用いてthemeを管理します。グローバルなuseStateを作成するイメージです。handleThemeChangeは受け取ったテーマ名(DARKなど)に応じてthemeを変更します。
また、themeの変更がされたときにLocal Storageにどのthemeが選択されたか保存しておきます。こうすることでリロードしても、テーマが維持されるようになります。
保存する方法は何でも構いません。CookieでもサーバーのDBに保存してもいいです。
useContextを使ってデフォルトで使用するテーマを設定
次は先程作ったcontextを使っていきます。App.js内に以下のコードを書きます。
const context = useContext(ThemeContext);
useEffect(() => {
const themeName = localStorage.getItem("theme");
if (themeName !== null) {
context.handleThemeChange(themeName);
}
}, [context]);React HooksのuseContextを使うことでcontextを簡単に使うことが出来ます。useEffectを使用し、レンダーされたときにLocal Storageに値があればテーマを変更します。
ここまででテーマを「セット」することは出来ました。ここからはテーマの「切り替え」です。
テーマを切り替えるためのメニューを設置
AppBarの以下の場所に「テーマを変更」Buttonを配置します。
<Typography variant="h6" className={classes.title}>
Demo
</Typography>
<Button
aria-controls="simple-menu"
aria-haspopup="true"
color="inherit"
onClick={handleClick}
>
テーマを変更
</Button>
</Toolbar>また、Material UIのメニューをCustomBar.jsの中に書きます。
<Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem onClick={() => handleMenuClick("DEFAULT")}>
デフォルト
</MenuItem>
<MenuItem onClick={() => handleMenuClick("DARK")}>ダーク</MenuItem>
<MenuItem onClick={() => handleMenuClick("NAVY")}>ネイビー</MenuItem>
</Menu>
テーマを切り替えられるようにする
以下のようにハンドラを設定します。themeContext.jsで設定したhandleThemeChangeハンドラをメニューがクリックされたときに呼び出しています。
const themeContext = useContext(ThemeContext);
const [anchorEl, setAnchorEl] = React.useState(null);
// テーマを変更ボタンがクリックされたとき
const handleClick = event => {
setAnchorEl(event.currentTarget);
};
// メニューのクローズ
const handleClose = () => {
setAnchorEl(null);
};
// メニューがクリックされたら、テーマを設定するハンドラを呼び出す。
// themeNameにはDARKなどの名前が入る。
const handleMenuClick = themeName => {
themeContext.handleThemeChange(themeName);
handleClose();
};以上で完成。
完成コード
上にも載せましたが、最終的なコードはこちらから↓
まとめ
Material UIでテーマをユーザが切り替えられるようにする方法を紹介しました。
React HooksのuseContextを使用しましたが、Reduxでも同様にstoreで管理すればいいだけです。今回はLocal Storageに保存しましたが、保存する先はCookieでもサーバーのデータベースでもどこでも構いません。
これからもReactに関する投稿をしていきますので、よかったらまた来て下さいね。

