React + TypeScript: ImmerのuseImmerReducerフックでリデューサを書く

[Immer](https://immerjs.github.io/immer/)はデータ構造をイミュータブル(不変)に保つためのライプラリです。React公式ドキュメントの作例でもたびたび使われています(「[オブジェクトをイミュータブルに保つ ー Immerを使う](https://qiita.com/FumioNonaka/items/d270e27a06db65609dcf#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E3%82%A4%E3%83%9F%E3%83%A5%E3%83%BC%E3%82%BF%E3%83%96%E3%83%AB%E3%81%AB%E4%BF%9D%E3%81%A4-%E3%83%BC-immer%E3%82%92%E4%BD%BF%E3%81%86)」など)。

Immerはイミュータブルなデータ構造を用いるさまざまな場面で使えます。たとえば、Reactの状態管理はそのひとつです。オブジェクトへの参照が変わっていなければ、オブジェクトもそのまま変更ありません。さらに、複製のコストは比較的低いです。データツリーの中で変更されていない部分は複製されることなく、以前の状態とメモリ上共有されます。Immerの基本的な使い方については、「[React + TypeScript: Immerで状態をイミュータブルに保つ](https://qiita.com/FumioNonaka/items/598f5064c498c9c816eb)」をお読みください。

本稿でご紹介するのは、リデューサが簡潔に書けるフック[useImmerReducer](https://immerjs.github.io/immer/example-setstate/#useimmerreducer)です(GitHub「[useImmerReducer](https://github.com/immerjs/use-immer#useimmerreducer)」。このREADME.mdの記述は少し古いのでご注意ください)。配列のミュータブルなメソッド`push()`や配列インデックスによる代入を用いつつ、状態はイミュータブルに保てます。


# React公式ドキュメントの作例をもとに
今回は、React公式サイトの`useReducer`が用いられた作例(「Extracting State Logic into a Reducer」の「[Step 3: Use the reducer from your component](https://react.dev/learn/extracting-state-logic-into-a-reducer#step-3-use-the-reducer-from-your-component)」に掲載)を、`useImmerReducer`で書き替えるというお題にしました。もっとも、TypeScriptは使われていないので、その修正を加えたのがつぎのサンプル001です。

**サンプル001■React + TypeScript: Extracting State Logic into a Reducer 02**
https://codesandbox.io/s/react-typescript-extracting-state-logic-into-a-reducer-02-etloeq

なお、この作例については「[React + TypeScript: 状態のロジックをリデューサに切り出す](https://qiita.com/FumioNonaka/items/6e7541567b38727d4f6c)」で解説しましたので、興味のある方はご参照ください。


# `useImmerReducer`フックを使う
`useImmerReducer`フックの構文は、`useReducer`と同じです。

```react
const [state, dispatch] = useImmerReducer(reducer, initialState);
```

1. 第1引数: リデューサ関数。
2. 第2引数: 状態初期値。
3. 戻り値: 配列。
    1. 第1要素: 現在の状態。
    2. 第2要素: `dispatch`関数(アクションをリデューサに送ります)。

すると、作例のアプリケーションモジュール`src/App.tsx`は、`import`して呼び出すフックを`useReducer`から`useImmerReducer`に置き替えるだけです。イベントハンドラからの`dispatch`の呼び出しは、そのままで構いません。`dispatch`の引数にアクションを渡すことは変わらないからです。

```react:src/App.tsx
// import { useReducer } from 'react';
import { useImmerReducer } from 'use-immer';

export default function TaskApp() {
	// const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
	const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks);

}
```

# リデューサ関数を書き替える
TypeScriptを使う場合、リデューサ関数`tasksReducer`の型づけが`Reducer`から`ImmerReducer`に変わります。関数の第1引数は状態をラップした`draft`オブジェクトです。第2引数はアクションのまま変わりません。第1引数は名前も`draft`としました。

```react:src/tasksReducer.ts
// import type { Reducer } from 'react';
import type { ImmerReducer } from 'use-immer';

// export const tasksReducer: Reducer<TaskType[], ActionType> = (
export const tasksReducer: ImmerReducer<TaskType[], ActionType> = (
	// tasks,
	draft,
	action
) => {

};
```

`draft`オブジェクトはミュータブルな構文で変更できます。状態はImmerがイミュータブルなまま保ってくれるからです。`draft`を直接変更したときには、`return`も要りません。ただし、`break`は忘れないでください。

```react:src/tasksReducer.ts
export const tasksReducer: ImmerReducer<TaskType[], ActionType> = (

) => {
	switch (action.type) {
		case 'added': {
			/* return [
				...tasks,
				{
					id: action.id,
					text: action.text,
					done: false
				}
			]; */
			draft.push({
				id: action.id,
				text: action.text,
				done: false
			});
			break;
		}
		case 'changed': {
			/* return tasks.map((_task) => {
				if (_task.id === action.task.id) {
					return action.task;
				} else {
					return _task;
				}
			}); */
			const index = draft.findIndex((_task) => _task.id === action.task.id);
			draft[index] = action.task;
			break;
		}
		case 'deleted': {
			// return tasks.filter((_task) => _task.id !== action.id);
			return draft.filter((_task) => _task.id !== action.id);
		}

};
```

こうして`useReducer`を`useImmerReducer`に書き替えたのが、つぎのサンプル002です。リデューサ関数(`tasksReducer`)の記述が少しすっきりしました。

**サンプル002■React + TypeScript: Extracting State Logic into a Reducer 03**
https://codesandbox.io/s/react-typescript-extracting-state-logic-into-a-reducer-03-yuwp0z

# Immerがやってくれること
リデューサは純粋でなければならないので、状態を変更してはいけません。けれど、Immerが提供するのは、変更しても安全な特別の`draft`オブジェクトです。内部的には、Immerがつくる状態のコピーに、`draft`への変更が適用されます。そのため、`useImmerReducer`が管理するリデューサは、第1引数(`draft`)を直接変更でき、状態は返さなくても済むのです。

# Immer公式作例をTypeScriptで型づけして書き直す
おまけとして、GitHubとImmer公式サイトのふたつの作例をTypeScriptで型づけして書き直しました。公式作例の前者は構文が古く、後者はコードに誤りがあったので、いずれも修正してあります。

GitHub「[useImmerReducer](https://github.com/immerjs/use-immer#useimmerreducer)」[Example](https://codesandbox.io/s/2zor1monvp):

**サンプル003■React + TypeScript: useImmerReducer 01**
https://codesandbox.io/s/react-typescript-useimmerreducer-01-40nmsi

Immer公式サイト「[useImmerReducer](https://immerjs.github.io/immer/example-setstate/#useimmerreducer)」[demo](https://codesandbox.io/s/useimmerreducer-sycpb?file=/src/index.js):

**サンプル004■React + TypeScript: useImmerReducer 02**
https://codesandbox.io/s/react-typescript-useimmerreducer-02-qmx5nh