React + TypeScript: コンテクストが渡すデータを更新するコード例

コンテクストのデータを更新するには、値はプロバイダが定められた親コンポーネントの状態変数にしてください。コンテクストのプロバイダに`value`値として渡すのはその状態変数です。

React公式ドキュメントの「[useContext](https://react.dev/reference/react/useContext)」には「[Updating data passed via context](https://react.dev/reference/react/useContext#updating-data-passed-via-context)」の項で、コンテクストの渡すデータを更新する5つのコードサンプルが紹介されています。もっとも、説明があまりありません(サンプルのコードを見てくださいということのようです)。そこで、本稿では解説をもう少し補ってみましょう。さらに、TypeScriptで型づけし、モジュール分けやコードについても少し工夫を加えました。

# チェックボックスによりコンポーネントのカラーを切り替える
つぎのサンプル001では、チェックボックスでコンポーネントのカラーが切り替わります。コードの中身については、「[コンテクストが渡すデータを更新する](https://qiita.com/FumioNonaka/items/8867a4c6182b2e15bb94#%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AF%E3%82%B9%E3%83%88%E3%81%8C%E6%B8%A1%E3%81%99%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B)」に解説しましたのでお読みください。

#### サンプル001■React + TypeScript: useContext 02
https://stackblitz.com/edit/react-typescript-usecontext-02?file=src%2FApp.tsx


# コンテクストによりオブジェクトを更新する
以下のサンプル002で、プロバイダの`value`に渡されるオブジェクトがプロパティとして備えるのはふたつの値です。

* `currentUser`: 状態を表すオブジェクト(`{ name: string }`)
* `setCurrentUser`: 状態を設定する関数(`() => void`)

それぞれ、親コンポーネント(`App`)の状態変数とその設定関数に定められています。

```react:src/App.tsx
export const CurrentUserContext = createContext<CurrentUserContextType>(
	defaultCurrentUserContext
);
function App() {
	const [currentUser, setCurrentUser] = useState({ name: '' });
	return (
		<CurrentUserContext.Provider
			value={{
				currentUser,
				setCurrentUser,
			}}
		>
			<Form />
		</CurrentUserContext.Provider>
	);
}
```

設定関数(`setCurrentUser`)により状態変数値(`currentUser`)を更新するのが、孫コンポーネント(`LoginButton`)のボタン(`Button`)です。ただし、設定値は決め打ち(`{ name: 'Advika' }`)なので、他の値には変わりません。

```react:src/LoginButton.tsx
export const LoginButton: FC = () => {
	const { currentUser, setCurrentUser } = useContext(CurrentUserContext);
	if (currentUser.name !== '') {
		return <p>You logged in as {currentUser.name}.</p>;
	}
	return (
		<Button
			onClick={() => {
				setCurrentUser({ name: 'Advika' });
			}}
		>
			Log in as Advika
		</Button>
	);
};
```

#### サンプル002■React + TypeScript: useContext example 01
https://stackblitz.com/edit/react-typescript-usecontext-example-01?file=src%2FApp.tsx


# 複数のコンテクストを定める
以下のルートモジュール`src/App.tsx`がつくるのはふたつのコンテクストです。子コンポーネントは、ふたつのプロバイダ(`Provider`)に包まれています。

* `ThemeContext`: 文字列で定められた要素のカラー(CSS)。
    - デフォルト値: `'light'`
* `CurrentUserContext`: 現在ログイン中のユーザー名を扱う状態変数と設定関数が収められたオブジェクト。
    - `currentUser`: 状態変数。
        - `{ name: string }`
    - `setCurrentUser`: 状態設定関数。
        - `(user: { name: string }) => void`

`ThemeContext`の値(`theme`)を切り替えるのは、チェックボックス(`<input type="checkbox">`)[Use dark mode]です。

```react:src/App.tsx
export const ThemeContext = createContext('light');

export const CurrentUserContext = createContext<CurrentUserContextType>(
	defaultCurrentUserContext
);
function App() {
	const [theme, setTheme] = useState('light');
	const [currentUser, setCurrentUser] = useState({ name: '' });
	return (
		<ThemeContext.Provider value={theme}>
			<CurrentUserContext.Provider
				value={{
					currentUser,
					setCurrentUser,
				}}
			>
				<WelcomePanel />
				<label>
					<input
						type="checkbox"
						checked={theme === 'dark'}
						onChange={({ target: { checked } }) => {
							setTheme(checked ? 'dark' : 'light');
						}}
					/>
					Use dark mode
				</label>
			</CurrentUserContext.Provider>
		</ThemeContext.Provider>
	);
}
```

モジュール`src/WelcomePanel.tsx`は、ログインユーザー名(`currentUser.name`)が設定済みかどうかで、表示する子コンポーネントを切り替えます。

```react:src/WelcomePanel.tsx
export const WelcomePanel: FC = () => {
	const { currentUser } = useContext(CurrentUserContext);
	return (
		<Panel title="Welcome">
			{currentUser.name !== '' ? <Greeting /> : <LoginForm />}
		</Panel>
	);
};
```

設定済みのユーザー名(`currentUser.name`)を表示するのが`src/Greeting.tsx`モジュールです。

```react:src/Greeting.tsx
export const Greeting = () => {
	const { currentUser } = useContext(CurrentUserContext);
	return <p>You logged in as {currentUser.name}.</p>;
};
```

ユーザー名が未設定のときは、モジュール`src/LoginForm.tsx`がテキストフィールドで入力を求めます。[Log in]ボタン(`<Button>`)でユーザー名登録のために呼び出すのは、コンテクスト(`CurrentUserContext`)から得た状態設定関数(`setCurrentUser`)です。

```react:src/LoginForm.tsx
export const LoginForm: FC = () => {
	const { setCurrentUser } = useContext(CurrentUserContext);

	return (
		<>

			<Button

				onClick={() => {
					setCurrentUser({
						name: firstName + ' ' + lastName,
					});
				}}
			>
				Log in
			</Button>

		</>
	);
};
```

コンテクスト`ThemeContext`が返す状態変数値`theme`は、CSSのクラス(`className`)によって要素のカラーを切り替えます。コンテクストを用いるモジュール`src/Panel.tsx`と`src/Button.tsx`のコンポーネントの定めは以下のとおりです。

```react:src/Panel.tsx
export const Panel: FC<Props> = ({ title, children }) => {
	const theme = useContext(ThemeContext);
	const className = 'panel-' + theme;
	return (
		<section className={className}>
			<h1>{title}</h1>
			{children}
		</section>
	);
};
```

```react:src/Button.tsx
export const Button: FC<Props> = ({ children, disabled, onClick }) => {
	const theme = useContext(ThemeContext);
	const className = 'button-' + theme;
	return (
		<button className={className} disabled={disabled} onClick={onClick}>
			{children}
		</button>
	);
};
```

つぎのサンプル003で。ユーザー名(`CurrentUserContext`)変更の[Log in]ボタンとカラー(`ThemeContext`)切り替えのチェックボックス[Use dark mode]がそれぞれ働いていることをお確かめください

#### サンプル003■React + TypeScript: useContext example 02
https://stackblitz.com/edit/react-typescript-usecontext-example-02?file=src%2FApp.tsx


# コンテクストプロバイダをコンポーネントに切り出す
アプリケーションが大きくなると、ルート近くでコンテクストプロバイダが子コンポーネントを幾重にも包むようになるかもしれません。それ自体は気にしなくてよいことです。けれど、プロバイダの入れ子をひとつのコンポーネントとして切り出せば、組み立てが整理できます。

前掲サンプル003のプロバイダは、たかだか二重の入れ子です。でも、この作例で、プロバイダコンポーネントを切り出してみましょう。プロバイダコンポーネント`src/MyProviders.tsx`はつぎのように定めます(`import`文や型づけおよびコンテクストのデフォルト値の定めは省きました。後掲サンプル004でお確かめください)。基本的には、ルートモジュール`src/App.tsx`からコードを抜き出しただけです。

```react:src/MyProviders.tsx
export const ThemeContext = createContext('light');

export const CurrentUserContext = createContext<CurrentUserContextType>(
	defaultCurrentUserContext
);
export const MyProviders: FC<Props> = ({ children, theme }) => {
	const [currentUser, setCurrentUser] = useState({ name: '' });
	return (
		<ThemeContext.Provider value={theme}>
			<CurrentUserContext.Provider
				value={{
					currentUser,
					setCurrentUser,
				}}
			>
				{children}
			</CurrentUserContext.Provider>
		</ThemeContext.Provider>
	);
};
```

すると、ルートモジュール`src/App.tsx`は、コンテクストプロバイダで二重にラップしていた子コンポーネントをプロバイダコンポーネント`MyProviders`で包めば済みます。コンテクストをつくるコード(`createContext`)も要りません。なお、状態変数`theme`と設定関数`setTheme`は、`App`コンポーネントから子(`<input type="checkbox"`)に渡すため残します。その場合、`MyProviders`には状態変数値をプロパティとして渡せばよいのです(前掲`MyProviders`コンポーネントが`props`として受け取っていることをお確かめください)。

```react:src/App.tsx
import { MyProviders } from './MyProviders';

/* export const ThemeContext = createContext('light');

export const CurrentUserContext = createContext<CurrentUserContextType>(
	defaultCurrentUserContext
); */
function App() {
	const [theme, setTheme] = useState('light');
	// const [currentUser, setCurrentUser] = useState({ name: '' });
	return (
		/* <ThemeContext.Provider value={theme}>
			<CurrentUserContext.Provider
				value={{
					currentUser,
					setCurrentUser,
				}}
			> */
		// <MyProviders theme={theme} setTheme={setTheme}>
		<MyProviders theme={theme}>
			<WelcomePanel />
			<label>
				<input
					type="checkbox"
					checked={theme === 'dark'}
					onChange={({ target: { checked } }) => {
						setTheme(checked ? 'dark' : 'light');
					}}
				/>
				Use dark mode
			</label>
		</MyProviders>
		/* </CurrentUserContext.Provider>
		</ThemeContext.Provider> */
	);
}
```

コンテクストを用いる子コンポーネントは、コンテクストの`import`もとパスの書き替えが必要です。

```react
// import { ThemeContext } from './App';
import { ThemeContext } from './MyProviders';
```

```react
// import { CurrentUserContext } from './App';
import { CurrentUserContext } from './MyProviders';
```

書き替えたアプリケーションの動きは前掲サンプル003と変わりません(サンプル004)。けれど、プロバイダコンポーネント`MyProviders`を切り出したことにより、ルートモジュール`src/App.tsx`のコードが整理されたでしょう。

#### サンプル004■React + TypeScript: useContext example 03
https://stackblitz.com/edit/react-typescript-usecontext-example-03?file=src%2FApp.tsx


# コンテクストとリデューサで状態に関わるロジックを切り出す
コンテクストに[リデューサ](https://ja.react.dev/reference/react/useReducer)を組み合わせると、状態に関わるロジックがコンポーネントから切り出せます(「[React + TypeScript: 状態のロジックをリデューサに切り出す](https://qiita.com/FumioNonaka/items/6e7541567b38727d4f6c)」参照)。

以下のサンプル005は、Todoリストの作例です。リデューサをプロバイダコンポーネントと組み合わせたコンテクストモジュール`src/TasksContext.tsx`は、つぎのように組み立てました。

```react:src/TasksContext.tsx
const tasksReducer: Reducer<TaskType[], ActionType> = (
	tasks,
	action
) => {
	switch (action.type) {
		case 'added': {

		}
		case 'changed': {

		}
		case 'deleted': {

		}
		default: {

		}
	}
};
export const TasksProvider: FC<Props> = ({ children }) => {
	const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
	return (
		<TasksContext.Provider value={tasks}>
			<TasksDispatchContext.Provider value={dispatch}>
				{children}
			</TasksDispatchContext.Provider>
		</TasksContext.Provider>
	);
};
```

注目いただきたいのは、コンテクスト(`TasksContext`と`TasksDispatchContext`)が`export`されていないことです。コンポーネントが必要とする状態変数`tasks`と`dispatch`関数は、カスタムフック`useTasksContext`から得られるようにしました。つまり、コンポーネントは直にコンテクストには触れません。

```react:src/TasksContext.tsx
const TasksContext = createContext<TaskType[] | null>(null);
const TasksDispatchContext = createContext<Dispatch<ActionType> | null>(null);

export const useTasksContext = () => {
	const tasks = useContext(TasksContext);
	const dispatch = useContext(TasksDispatchContext);
	return { tasks, dispatch };
};
```

コンテクストモジュール`src/TasksContext.tsx`の切り出しにより、きわめて簡潔になったルートモジュール`src/TaskApp.tsx`ののコードはつぎのとおりです。それぞれのモジュールの記述全体とアプリケーションの動きは、以下のサンプル005でお確かめください。この作例のコードについては、「[React + TypeScript: リデューサとコンテクストで拡張性を高める](https://qiita.com/FumioNonaka/items/efc7db380a120f99d4bb)」で段階を踏んで解説しています。

```react:src/TaskApp.tsx
export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}
```

#### サンプル005■React + TypeScript: Scaling Up with Reducer and Context 04
https://stackblitz.com/edit/react-typescript-scaling-up-with-reducer-and-context-04?file=src%2FApp.tsx