テーマの全体共有
切り替えられるテーマにする
ここまでのテーマは const theme: Theme = "dark" と書いた固定値でした。本物のダークモードは、ボタンを押すと切り替わります。切り替わるものは state です。
問題は、その state をどこに置くかです。App に置くと、切り替える関数も App にあります。それをボタンまで届けるのに、また props のバケツリレーが始まります。
Provider ごと部品にする
そこで、state と切り替えの関数をまとめて持つ部品を作ります。中身を children で受け取り、Provider で包んで返すだけの部品です。
export function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>("light");
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}value にオブジェクトを入れて、値と関数を一緒に配るのがこつです。読むほうだけ配ると、切り替えたい部品が困ります。
箱の型もそれに合わせます。
export type ThemeContextValue = {
theme: Theme;
toggleTheme: () => void;
};
export const ThemeContext = createContext<ThemeContextValue>({
theme: "light",
toggleTheme: () => {},
});既定値の toggleTheme は何もしない関数です。Provider の外で呼ばれても落ちないようにするための置き石だと思ってください。
使う側は分割代入で
const { theme, toggleTheme } = useContext(ThemeContext);これでボタンの onClick に toggleTheme をそのまま渡せます。ヘッダーも価格も、同じ箱を覗くだけで足並みがそろいます。
自分が置いた Provider の中は覗けない
一つ落とし穴があります。<ThemeProvider> を描いている App 自身は、その Provider の外側にいます。App の中で useContext を呼んでも既定値しか返りません。
だから、画面の本体は App の中に直接書かず、Screen のような部品に切り出して Provider の中に入れます。値を配る人と、値を使う人を分けるのが Provider 設計の基本形です。
演習
ThemeProvider を作り、ボタンで画面全体のテーマを切り替えます。