0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

テーマ切り替えのためのProviderとSwitcherコンポーネントを実装する

Posted at

  • 前回の記事で書いたテーマスイッチャーはボタンの位置が固定でいまいちだったので、ボタンの位置を変えられるようにします。
    • 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つのコンポーネントを実装します。
  1. Theme.tsx
    • このコンポーネントはテーマをContextとして管理します。
  2. Theme.Provider.tsx
    • このコンポーネント内にテーマが反映されます。
  3. 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-50text-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を当ててますが、スタイルを強制するのは良くないかもしれない。
    - nameOnLightnameOnDarkはそれぞれ、ライトテーマ時のボタン名とダークテーマ時のボタン名を指定します。
'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

  • コンポーネントをドット記法で参照できるように、ThemeProviderSwitcherを生やします。
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.ProviderTheme内に複数あっても良いので、テーマが適用されないブロックを間に挟むこともできます。
    • テーマが適用されないブロックにTheme.Switcherを置いてもいけます。

終わりに

  • テーマ切り替えボタンと、テーマを反映する箇所をある程度コントロールできるコンポーネントができました。
    • ナビゲーションバーを実装する時にボタンの位置が固定では使いづらいので、自由に動かせるようになって概ね満足しています。

謝辞

  • Lorem JPsum - ダミーテキスト作成に利用しました。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?