序
- 前回の記事で書いたテーマスイッチャーはボタンの位置が固定でいまいちだったので、ボタンの位置を変えられるようにします。
-
tw-colors
という、className
にテーマ名を設定するだけで切り替えができるTailwind CSS用プラグインを使っています。
-
前回の実装
// ./app/components/ThemeSwitcher.tsx
'use client';
import { ReactNode, useState } from 'react';
export default function ThemeSwitcher(props: { children: ReactNode }) {
const [isDark, setIsDark] = useState(false);
return (
<div className={`${isDark ? 'dark' : 'light'}`}>
<div className="w-full bg-primary-50 text-center">
<button
className="mx-auto my-4 mb-12 w-96 rounded-xl bg-primary p-2 font-bold uppercase text-on-primary shadow-xl"
onClick={() => setIsDark(!isDark)}
>
{`Switch to ${!isDark ? 'dark' : 'light'} theme.`}
</button>
</div>
{props.children}
</div>
);
}
動作サンプル
実装
- 今回は3つのコンポーネントを実装します。
-
Theme.tsx
- このコンポーネントはテーマをContextとして管理します。
-
Theme.Provider.tsx
- このコンポーネント内にテーマが反映されます。
-
Theme.Switcher.tsx
- このコンポーネントはテーマ切り替えボタンを提供します。
-
Theme
コンポーネント内であれば、Provider
の外に置いてもOKです。
Theme.tsx
-
createContext()
でテーマの状態(isDark
)とステート更新用関数(setIsDark
)をコンテキスト化します。- 更新用関数がコンテキストにあることで、子コンポーネントの
Switcher
がテーマを切り替えることができます。
- 更新用関数がコンテキストにあることで、子コンポーネントの
'use client';
import {
Dispatch,
ReactNode,
SetStateAction,
createContext,
useContext,
useState,
} from 'react';
export type ThemeComponents = {
(props: { children: ReactNode }): JSX.Element;
Provider: (props: { children: ReactNode; className?: string }) => JSX.Element;
Switcher: (props?: {
className?: string;
nameOnLight?: string;
nameOnDark?: string;
}) => JSX.Element;
};
export const ThemeContext = createContext({
isDark: false,
setIsDark: (() => {}) as Dispatch<SetStateAction<boolean>>,
});
const Theme = (props: { children: ReactNode }) => {
const [isDark, setIsDark] = useState(useContext(ThemeContext).isDark);
return (
<ThemeContext.Provider value={{ isDark, setIsDark }}>
{props.children}
</ThemeContext.Provider>
);
};
export default Theme as ThemeComponents;
Theme.Provider.tsx
- コンテキストから
isDark
を引っこ抜いて、クラス名に反映します。-
bg-primary-50
とtext-on-primary-50
を強制的に適用していますが、中途半端なので、完全にヘッドレスコンポーネントにした方がいいかもしれません。
-
'use client';
import { ReactNode, useContext } from 'react';
import { ThemeContext } from '@/components/Theme/Theme';
export default function Provider(props: {
children: ReactNode;
className?: string;
}) {
const className = props.className ?? '';
const { isDark } = useContext(ThemeContext);
return (
<div
className={`${
isDark ? 'dark' : 'light'
} bg-primary-50 text-on-primary-50 ${className}`}
>
{props.children}
</div>
);
}
Theme.Switcher.tsx
- コンテキストから
setIsDark
を引っこ抜いて、onClick
でテーマを切り替えます。
- これも、Provider
内に置かれた時にいい感じの見た目になるようにclassName
を当ててますが、スタイルを強制するのは良くないかもしれない。
-nameOnLight
とnameOnDark
はそれぞれ、ライトテーマ時のボタン名とダークテーマ時のボタン名を指定します。
'use client';
import { useContext } from 'react';
import { ThemeContext } from '@/components/Theme/Theme';
export default function Switcher(props?: {
className?: string;
nameOnLight?: string;
nameOnDark?: string;
}) {
const className =
props?.className ??
'mx-auto rounded-xl bg-primary px-4 py-2 font-bold uppercase text-on-primary shadow-xl';
const nameOnLight = props?.nameOnLight ?? 'Switch to dark theme.';
const nameOnDark = props?.nameOnDark ?? 'Switch to light theme.';
const { isDark, setIsDark } = useContext(ThemeContext);
return (
<button
className={className}
onClick={() => {
setIsDark(!isDark);
}}
>
{isDark ? nameOnDark : nameOnLight}
</button>
);
}
index.tsx
- コンポーネントをドット記法で参照できるように、
Theme
にProvider
とSwitcher
を生やします。
import Theme from '@/components/Theme/Theme';
import Provider from '@/components/Theme/Theme.Provider';
import Switcher from '@/components/Theme/Theme.Switcher';
Theme.Provider = Provider;
Theme.Switcher = Switcher;
export default Theme;
- ドット記法でコンポーネントを生やしたコンポーネントを
export
する場合、最終的に"use client";
がないファイルからexport
しないと、実行時にUnsupported Server Component type: undefined
というエラーが出て、Server Componentから使う時にうまく動かないみたいです。
使い方
- こんな感じです。
<Theme>
<Theme.Provider>
{/*ここはテーマが反映される*/}
</Theme.Provider>
{/*ここはされない*/}
<Theme.Provider>
{/*ここはテーマが反映されるので、Switcherはテーマの影響を受ける*/}
<Theme.Switcher />
{props.children}
</Theme.Provider>
{/*ここはテーマが反映されないので、Switcherをここに置くとテーマの影響を受けない*/}
<Theme.Switcher />
</Theme>
サンプル
- サンプルでは、左右で別の
Theme
コンポーネントを使ってるので、それぞれ独立してテーマの変更ができます。- あんまり使い所はなさそうですが……。
-
Theme.Provider
はTheme
内に複数あっても良いので、テーマが適用されないブロックを間に挟むこともできます。- テーマが適用されないブロックに
Theme.Switcher
を置いてもいけます。
- テーマが適用されないブロックに
終わりに
- テーマ切り替えボタンと、テーマを反映する箇所をある程度コントロールできるコンポーネントができました。
- ナビゲーションバーを実装する時にボタンの位置が固定では使いづらいので、自由に動かせるようになって概ね満足しています。
謝辞
- Lorem JPsum - ダミーテキスト作成に利用しました。