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

# 序

- 前回の記事で書いたテーマスイッチャーはボタンの位置が固定でいまいちだったので、ボタンの位置を変えられるようにします。
    - `tw-colors`という、`className`にテーマ名を設定するだけで切り替えができるTailwind CSS用プラグインを使っています。

https://qiita.com/kedama-t/items/26284af2e961f7c3acbc

<details><summary>前回の実装</summary>

```tsx
// ./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>
  );
}
```
</details>

## 動作サンプル

https://stackblitz.com/edit/stackblitz-starters-hwoj2h?file=README.md&view=editor

## 実装

- 今回は3つのコンポーネントを実装します。

1. `Theme.tsx`
    - このコンポーネントはテーマをContextとして管理します。
2. `Theme.Provider.tsx`
    - このコンポーネント内にテーマが反映されます。
3. `Theme.Switcher.tsx`
    - このコンポーネントはテーマ切り替えボタンを提供します。
    - `Theme`コンポーネント内であれば、`Provider`の外に置いてもOKです。

### Theme.tsx

- `createContext()`でテーマの状態(`isDark`)とステート更新用関数(`setIsDark`)をコンテキスト化します。
    - 更新用関数がコンテキストにあることで、子コンポーネントの`Switcher`がテーマを切り替えることができます。

```tsx
'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`を強制的に適用していますが、中途半端なので、完全にヘッドレスコンポーネントにした方がいいかもしれません。

```tsx
'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`はそれぞれ、ライトテーマ時のボタン名とダークテーマ時のボタン名を指定します。

```tsx
'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`を生やします。

```tsx
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から使う時にうまく動かないみたいです。

https://stackoverflow.com/questions/75261466/unsupported-server-component-type-undefined-next-js-13

## 使い方

- こんな感じです。

```tsx
  <Theme>
      <Theme.Provider>
        {/*ここはテーマが反映される*/}
      </Theme.Provider>
      {/*ここはされない*/}
      <Theme.Provider>
        {/*ここはテーマが反映されるので、Switcherはテーマの影響を受ける*/}
        <Theme.Switcher />
        {props.children}
      </Theme.Provider>
      {/*ここはテーマが反映されないので、Switcherをここに置くとテーマの影響を受けない*/}
      <Theme.Switcher />
  </Theme>
```

### サンプル

- サンプルでは、左右で別の`Theme`コンポーネントを使ってるので、それぞれ独立してテーマの変更ができます。
  - あんまり使い所はなさそうですが……。

![動作サンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524683/15ca2a0e-daf9-e892-7cc9-4eabfc55840b.png)

- `Theme.Provider`は`Theme`内に複数あっても良いので、テーマが適用されないブロックを間に挟むこともできます。
    - テーマが適用されないブロックに`Theme.Switcher`を置いてもいけます。

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

### 謝辞

- [Lorem JPsum](https://lorem.sabigara.com/) - ダミーテキスト作成に利用しました。