1
1

More than 1 year has passed since last update.

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

Posted at

Immerはデータ構造をイミュータブル(不変)に保つためのライプラリです。React公式ドキュメントの作例でもたびたび使われています(「オブジェクトをイミュータブルに保つ ー Immerを使う」など)。

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

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

React公式ドキュメントの作例をもとに

今回は、React公式サイトのuseReducerが用いられた作例(「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: 状態のロジックをリデューサに切り出す」で解説しましたので、興味のある方はご参照ください。

useImmerReducerフックを使う

useImmerReducerフックの構文は、useReducerと同じです。

const [state, dispatch] = useImmerReducer(reducer, initialState);
  1. 第1引数: リデューサ関数。
  2. 第2引数: 状態初期値。
  3. 戻り値: 配列。
    1. 第1要素: 現在の状態。
    2. 第2要素: dispatch関数(アクションをリデューサに送ります)。

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

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としました。

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は忘れないでください。

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);
		}

};

こうしてuseReduceruseImmerReducerに書き替えたのが、つぎのサンプル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「useImmerReducerExample:

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

Immer公式サイト「useImmerReducerdemo:

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

1
1
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
1
1