React + TypeScript: Immerで状態をイミュータブルに保つ

[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)」など)。また、2019年にはつぎのような賞を獲得しました。

* [Breakthrough of the year](https://osawards.com/react/2019) ー React open source award
* [The Most Impactful Contribution to the community](https://osawards.com/javascript/2019) ー JavaScript open source award

Immerはイミュータブルなデータ構造を用いるさまざまな場面で使えます。たとえば、Reactの状態管理はそのひとつです。オブジェクトへの参照が変わっていなければ、オブジェクトもそのまま変更ありません。さらに、複製のコストは比較的低いです。データツリーの中で変更されていない部分は複製されることなく、以前の状態とメモリ上共有されます。

イミュータブルなデータ構造により得られるのがこうした利点です。けれど、手作業でいちいち複製をつくるのは、扱いづらいコードになりやすく、漏れが生じるかもしれません。この課題を解決するのがImmerです。

なお、ReactにかぎらないImmerの基本的な使い方と構文については、「[JavaScript + Immer: データ構造をイミュータブルに保つ ー 使うのはイマでしょ！](https://qiita.com/FumioNonaka/items/6231ee7a540df165c413)」をお読みください。

# React + TypeScriptによる簡単なTodoリストのコード例
`useState`フックが内部的にもっている状態は、イミュータブル(不変)として扱われるのが前提です。まずは、React + TypeScriptで簡単な(手抜きの)Todoリストをコード例として書いてみます(まだImmerは使いません)。Todo項目をただ一覧表示するだけです。モジュール`src/Todo.tsx`は、チェックボックス(`<input type="checkbox">`)をクリックすると、親からプロパティで受け取った`onToggle`に`todo.id`を渡して呼び出します。

#### コード001■Todo項目の表示モジュール
```react:src/Todo.tsx
import { FC, memo } from 'react';
import type { TodoItem } from './App';

type Props = {
	onToggle: (id: string) => void;
	todo: TodoItem;
};
export const Todo: FC<Props> = memo(({ todo, onToggle }) => (
	<li>
		<input
			type="checkbox"
			checked={todo.done}
			onChange={() => onToggle(todo.id)}
		/>
		{todo.title}
	</li>
));
```

親モジュール`src/App.tsx`は、まだImmerを使っていません。子の`Todo`コンポーネントにプロパティ(`onToggle`)として与えた`handleToggle()`は、状態変数(`todos`)の値を[スプレッド構文`...`](https://qiita.com/FumioNonaka/items/20e96ca9ede2ae157bcc#%E3%82%B9%E3%83%97%E3%83%AC%E3%83%83%E3%83%89%E6%A7%8B%E6%96%87)により複製して変更したうえで、設定関数(`setTodos()`)に渡しています(「[データを変更しないことの効果](https://ja.reactjs.org/docs/optimizing-performance.html#the-power-of-not-mutating-data)」参照)。

```react:src/App.tsx
import React, { useCallback, useState } from 'react';
import { Todo } from './Todo';

export type TodoItem = {
	id: string;
	title: string;
	done: boolean;
};
let nextId = 0;
const getNextId = () => String(nextId++);
const initialTodos: TodoItem[] = [
	{ id: getNextId(), title: 'Learn React', done: true },
	{ id: getNextId(), title: 'Try immer', done: false }
];
function App() {
	const [todos, setTodos] = useState(initialTodos);
	const handleToggle = useCallback(
		(id: string) => {
			const nextTodos = [...todos];
			const todo = nextTodos.find((todo) => todo.id === id);
			if (!todo) return;
			todo.done = !todo.done;
			setTodos(nextTodos);
		},
		[todos]
	);
	return (
		<div className="App">
			<ul>
				{todos.map((todo) => (
					<Todo todo={todo} key={todo.id} onToggle={handleToggle} />
				))}
			</ul>
		</div>
	);
}
export default App;
```


# Immerで状態を管理する
状態はイミュータブルなまま、深い階層まで更新できるのがImmerです。つぎのようにインストールしますYarnやCDNによるインストール、あるいはカスタマイズについては公式サイトの「[Installation](https://immerjs.github.io/immer/installation)」をご参照ください。

```terminal
npm install immer
```

Immerの主軸となる関数が[`produce()`](https://immerjs.github.io/immer/produce)です。イミュータブルな(変更しない)状態からつくったミュータブルな(変更できる)状態に変更を加えます。構文はつぎのとおりで、引数(`recipe`)は状態をどう変更するか定める関数です。戻り値(`mutationFunction`)も関数で、ミュータブルに変換した状態に変更を加えます(「[カリー化したproduce()関数](https://qiita.com/FumioNonaka/items/6231ee7a540df165c413#%E3%82%AB%E3%83%AA%E3%83%BC%E5%8C%96%E3%81%97%E3%81%9Fproduce%E9%96%A2%E6%95%B0)」参照)。

```react
produce(recipe: (draftState) => void): mutationFunction
```

* `recipe`: `produce`の引数となる関数。戻り値の`mutationFunction`が引数に受け取った状態をどう「変更」するか定める。
* `mutationFunction`: 引数に受け取った状態を`recipe`にしたがって変更する。


```react
mutationFunction(baseState): nextState
```

* `baseState`: 変更を加えるもととなるイミュータブルな状態。
* `nextState`: もとの状態`baseState`のプロキシに、`recipe`の変更を安全に加えた状態。

前項のモジュール`src/App.tsx`の記述を`produce()`関数により書き替えたのがつぎのコードです。前掲のコードは設定関数`setTodos()`に値(`nextTodos`)を渡しているのに対して、`produce()`の戻り値による「[関数型の更新](https://ja.reactjs.org/docs/hooks-reference.html#functional-updates)」になっていることにご注目ください。つまり、`draft`として受け取るもとの参照は、直近の状態変数(`todos`)の値です。

```react:src/App.tsx
import produce from 'immer';

function App() {

	const handleToggle = useCallback(
		(id: string) => {
			/* const nextTodos = [...todos];
			const todo = nextTodos.find((todo) => todo.id === id);
			if (!todo) return;
			todo.done = !todo.done;
			setTodos(nextTodos); */
			const nextTodos = produce((draft) => {
				const todo = draft.find((todo: TodoItem) => todo.id === id);
				todo.done = !todo.done;
			});

		},
		// [todos]
		[]
	);

}
```


# Todo項目追加のコンポーネントを定める
テキスト入力フィールドとボタンが備わった、Todo項目追加のコンポーネントをつくりましょう。つぎのコード002が、アプリケーションに新たに加えるモジュール`src/TodoInput.tsx`です。ボタンクリック(`onClick`)のイベントハンドラ`handleClick()`から呼び出す項目追加の関数`handleAdd()`は、親コンポーネントからプロパティとして受け取ります。引数はテキストフィールドに入力された文字列(`title`)です。

#### コード002■Todo項目追加のモジュール
```react:src/TodoInput.tsx
import { FC, useCallback, useState } from 'react';

type Props = {
	handleAdd: (title: string) => void;
};
export const TodoInput: FC<Props> = ({ handleAdd }) => {
	const [title, setTitle] = useState('');
	const handleClick = useCallback(() => {
		handleAdd(title);
		setTitle('');
	}, [handleAdd, title]);
	return (
		<p>
			<input
				type="text"
				value={title}
				onChange={(event) => setTitle(event.target.value)}
			/>
			<button type="button" onClick={handleClick}>
				Add Todo
			</button>
		</p>
	);
};
```

親モジュール`src/App.tsx`に定める項目追加の関数`handleAdd()`は、つぎのコードのとおりです。子コンポーネント`TodoInput`にプロパティ(`handleAdd`)として加えます。設定関数(`setTodos()`)に渡す引数のTodo項目を加えたリストの配列には、一旦スプレッド構文`...`を用いました。

```react:src/App.tsx
import { TodoInput } from "./TodoInput";

function App() {

	const handleAdd = useCallback(
		(todoTitle: string) => {
			const title = todoTitle.trim();
			if (!title) return;
			const newTodo = {
				id: getNextId(),
				title,
				done: false
			};
			setTodos([...todos, newTodo]);
		},
		[todos]
	);

	return (
		<div className="App">
			<TodoInput handleAdd={handleAdd} />

		</div>
	);
}
```

スプレッド構文`...`を`produce()`関数に書き替えたのがつぎのコードです。

```react:src/App.tsx
function App() {
	const [todos, setTodos] = useState(initialTodos);
	const handleAdd = useCallback(
		(todoTitle: string) => {

			// setTodos([...todos, newTodo]);
			setTodos(produce((draft) => {
				draft.push(newTodo)
			}));
		},
		// [todos]
		[]
	);

}
```

書き上がったルートモジュール`src/App.tsx`の記述をつぎのコード003にまとめました。以下のサンプル001がCodeSandboxに公開した作例です。各モジュールのコードや動きは、このサンプルでお確かめください。

#### コード003■ルートモジュール
```react:src/App.tsx
import React, { useCallback, useState } from 'react';
import produce from 'immer';
import { Todo } from './Todo';
import { TodoInput } from './TodoInput';
import './styles.css';

export type TodoItem = {
	id: string;
	title: string;
	done: boolean;
};
let nextId = 0;
const getNextId = () => String(nextId++);
const initialTodos: TodoItem[] = [
	{ id: getNextId(), title: 'Learn React', done: true },
	{ id: getNextId(), title: 'Try immer', done: false }
];
function App() {
	const [todos, setTodos] = useState(initialTodos);
	const handleAdd = useCallback((todoTitle: string) => {
		const title = todoTitle.trim();
		if (!title) return;
		const newTodo = {
			id: getNextId(),
			title,
			done: false
		};
		setTodos(
			produce((draft) => {
				draft.push(newTodo);
			})
		);
	}, []);
	const handleToggle = useCallback((id: string) => {
		const nextTodos = produce((draft) => {
			const todo = draft.find((todo: TodoItem) => todo.id === id);
			todo.done = !todo.done;
		});
		setTodos(nextTodos);
	}, []);
	return (
		<div className="App">
			<TodoInput handleAdd={handleAdd} />
			<ul>
				{todos.map((todo) => (
					<Todo todo={todo} key={todo.id} onToggle={handleToggle} />
				))}
			</ul>
		</div>
	);
}
export default App;
```

#### サンプル001■React + TypeScript: Management of immutable state with Immer 01
https://codesandbox.io/s/react-typescript-management-of-immutable-state-with-immer-01-kd7x0w


# フック`useImmer`を使う
モジュール[`use-immer`](https://github.com/immerjs/use-immer#use-immer)には、Reactで使いやすいようにフック`useImmer`が備わっています。`produce()`関数は直接は用いません。

```terminal
npm install use-immer
```

前掲コード003のモジュールsrc/App.tsxの記述はつぎのように改めます。

```react:src/App.tsx
// import React, { useCallback, useState } from 'react';
import { useCallback } from 'react';
// import produce from 'immer';
import { useImmer } from 'use-immer';

function App() {
	// const [todos, setTodos] = useState(initialTodos);
	const [todos, setTodos] = useImmer(initialTodos);
	const handleAdd = useCallback(
		(todoTitle: string) => {

			setTodos(
				// produce((draft) => {
				(draft) => {
					draft.push(newTodo);
					// })
				}
			);
			// }, []);
		},
		[setTodos]
	);
	const handleToggle = useCallback(
		(id: string) => {
			/* const nextTodos = produce((draft) => {
			const todo = draft.find((todo: TodoItem) => todo.id === id);
			todo.done = !todo.done;
		}); */
			// setTodos(nextTodos);
			setTodos((draft) => {
				const todo = draft.find((todo: TodoItem) => todo.id === id);
				if (todo) {
					todo.done = !todo.done;
				}
			});
			// }, []);
		},
		[setTodos]
	);

}
```

ルートモジュール`src/App.tsx`の記述全体は、つぎのコード004のとおりです。併せて、以下のサンプル002をCodeSandboxに掲げます。

#### コード004■ルートモジュール
```react:src/App.tsx
import { useCallback } from 'react';
import { useImmer } from 'use-immer';
import { Todo } from './Todo';
import { TodoInput } from './TodoInput';
import './styles.css';

export type TodoItem = {
	id: string;
	title: string;
	done: boolean;
};
let nextId = 0;
const getNextId = () => String(nextId++);
const initialTodos: TodoItem[] = [
	{ id: getNextId(), title: 'Learn React', done: true },
	{ id: getNextId(), title: 'Try immer', done: false }
];
function App() {
	const [todos, setTodos] = useImmer(initialTodos);
	const handleAdd = useCallback(
		(todoTitle: string) => {
			const title = todoTitle.trim();
			if (!title) return;
			const newTodo = {
				id: getNextId(),
				title,
				done: false
			};
			setTodos((draft) => {
				draft.push(newTodo);
			});
		},
		[setTodos]
	);
	const handleToggle = useCallback(
		(id: string) => {
			setTodos((draft) => {
				const todo = draft.find((todo: TodoItem) => todo.id === id);
				if (todo) {
					todo.done = !todo.done;
				}
			});
		},
		[setTodos]
	);
	return (
		<div className="App">
			<TodoInput handleAdd={handleAdd} />
			<ul>
				{todos.map((todo) => (
					<Todo todo={todo} key={todo.id} onToggle={handleToggle} />
				))}
			</ul>
		</div>
	);
}
export default App;
```

#### サンプル002■React + TypeScript: Management of immutable state with Immer 02
https://codesandbox.io/s/react-typescript-management-of-immutable-state-with-immer-02-tcvx42


# フック`useReducer`に`produce()`関数を使う
`produce()`関数はフック`useReducer`にも使えます。書き替えるもとにするのは、`useImmer`を使う前のコード003のモジュール`src/App.tsx`にしましょう。`useReducer`の第1引数に渡すのが、`product()`関数の呼び出しです。状態の初期値(`initialTodos`)は第2引数として与えます。

```react:src/App.tsx
// import React, { useCallback, useState } from 'react';
import React, { useCallback, useReducer } from 'react';

type ActionType = 'add' | 'toggle';
type Action = {
	type: ActionType;
	title?: string;
	id?: string;
};

function App() {
	// const [todos, setTodos] = useState(initialTodos);
	const [todos, dispatch] = useReducer(
		produce((draft, action: Action) => {
			switch (action.type) {
				case 'add':
					const newTodo = {
						id: getNextId(),
						title: action.title,
						done: false
					};
					draft.push(newTodo);
					break;
				case 'toggle':
					const todo = draft.find((todo: TodoItem) => todo.id === action.id);
					todo.done = !todo.done;
					break;
				default:
					break;
			}
		}),
		initialTodos
	);

}
```

`App`コンポーネントの関数は、設定関数(`setTodos()`)でなく、`dispatch()`関数を呼び出さなければなりません。

```react:src/App.tsx
function App() {

	const handleAdd = useCallback((todoTitle: string) => {

		/* const newTodo = {
			id: getNextId(),
			title,
			done: false
		};
		setTodos(
			produce((draft) => {
				draft.push(newTodo);
			})
		); */
		dispatch({
			type: 'add',
			title
		});
	}, []);
	const handleToggle = useCallback((id: string) => {
		/* const nextTodos = produce((draft) => {
			const todo = draft.find((todo: TodoItem) => todo.id === id);
			todo.done = !todo.done;
		});
		setTodos(nextTodos); */
		dispatch({ type: 'toggle', id });
	}, []);

}
```

書き改めたルートモジュール`src/App.tsx`の記述全体は、つぎのコード005のとおりです。CodeSandbox作例は以下のサンプル003に掲げました。

#### コード005■ルートモジュール
```react:src/App.tsx
import React, { useCallback, useReducer } from 'react';
import produce from 'immer';
import { Todo } from './Todo';
import { TodoInput } from './TodoInput';
import './styles.css';

export type TodoItem = {
	id: string;
	title: string;
	done: boolean;
};
type ActionType = 'add' | 'toggle';
type Action = {
	type: ActionType;
	title?: string;
	id?: string;
};
let nextId = 0;
const getNextId = () => String(nextId++);
const initialTodos: TodoItem[] = [
	{ id: getNextId(), title: 'Learn React', done: true },
	{ id: getNextId(), title: 'Try immer', done: false }
];
function App() {
	const [todos, dispatch] = useReducer(
		produce((draft, action: Action) => {
			switch (action.type) {
				case 'add':
					const newTodo = {
						id: getNextId(),
						title: action.title,
						done: false
					};
					draft.push(newTodo);
					break;
				case 'toggle':
					const todo = draft.find((todo: TodoItem) => todo.id === action.id);
					todo.done = !todo.done;
					break;
				default:
					break;
			}
		}),
		initialTodos
	);
	const handleAdd = useCallback((todoTitle: string) => {
		const title = todoTitle.trim();
		if (!title) return;
		dispatch({
			type: 'add',
			title
		});
	}, []);
	const handleToggle = useCallback((id: string) => {
		dispatch({ type: 'toggle', id });
	}, []);
	return (
		<div className="App">
			<TodoInput handleAdd={handleAdd} />
			<ul>
				{todos.map((todo: TodoItem) => (
					<Todo todo={todo} key={todo.id} onToggle={handleToggle} />
				))}
			</ul>
		</div>
	);
}
export default App;
```

#### サンプル003■React + TypeScript: Management of immutable state with Immer 03
https://codesandbox.io/s/react-typescript-management-of-immutable-state-with-immer-03-vhnsuu


# フック`useImmerReducer`を使う
すでにご紹介した`use-immer`モジュールには、フック`useImmerReducer`が備わっています。このフックを用いれば、前掲コード005はつぎのように少しだけ短く書けるのです。`produce()`関数は、直接は使わなくなりました。

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

function App() {
	// const [todos, dispatch] = useReducer(
	const [todos, dispatch] = useImmerReducer(
		// produce((draft, action: Action) => {
		(draft, action: Action) => {

			}
			// }),
		},

	);
	const handleAdd = useCallback(
		(todoTitle: string) => {

			// }, []);
		},
		[dispatch]
	);
	const handleToggle = useCallback(
		(id: string) => {

			// }, []);
		},
		[dispatch]
	);

}
```

修正箇所は少ないものの、ルートモジュール`src/App.tsx`の記述全体をつぎのコード006にまとめておきましょう。CodeSandbox作例は以下のサンプル004として公開しました。

#### コード006■ルートモジュール
```react:src/App.tsx
import React, { useCallback } from 'react';
import { useImmerReducer } from 'use-immer';
import { Todo } from './Todo';
import { TodoInput } from './TodoInput';
import './styles.css';

export type TodoItem = {
	id: string;
	title: string;
	done: boolean;
};
type ActionType = 'add' | 'toggle';
type Action = {
	type: ActionType;
	title?: string;
	id?: string;
};
let nextId = 0;
const getNextId = () => String(nextId++);
const initialTodos: TodoItem[] = [
	{ id: getNextId(), title: 'Learn React', done: true },
	{ id: getNextId(), title: 'Try immer', done: false }
];
function App() {
	const [todos, dispatch] = useImmerReducer((draft, action: Action) => {
		switch (action.type) {
			case 'add':
				const newTodo = {
					id: getNextId(),
					title: action.title,
					done: false
				};
				draft.push(newTodo);
				break;
			case 'toggle':
				const todo = draft.find((todo: TodoItem) => todo.id === action.id);
				todo.done = !todo.done;
				break;
			default:
				break;
		}
	}, initialTodos);
	const handleAdd = useCallback(
		(todoTitle: string) => {
			const title = todoTitle.trim();
			if (!title) return;
			dispatch({
				type: 'add',
				title
			});
		},
		[dispatch]
	);
	const handleToggle = useCallback(
		(id: string) => {
			dispatch({ type: 'toggle', id });
		},
		[dispatch]
	);
	return (
		<div className="App">
			<TodoInput handleAdd={handleAdd} />
			<ul>
				{todos.map((todo: TodoItem) => (
					<Todo todo={todo} key={todo.id} onToggle={handleToggle} />
				))}
			</ul>
		</div>
	);
}
export default App;
```

#### サンプル004■React + TypeScript: Management of immutable state with Immer 04
https://codesandbox.io/s/react-typescript-management-of-immutable-state-with-immer-04-9s4d00?file=/src/App.tsx