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引数: リデューサ関数。
- 第2引数: 状態初期値。
- 戻り値: 配列。
- 第1要素: 現在の状態。
- 第2要素:
dispatch
関数(アクションをリデューサに送ります)。
すると、作例のアプリケーションモジュールsrc/App.tsx
は、import
して呼び出すフックをuseReducer
からuseImmerReducer
に置き替えるだけです。イベントハンドラからのdispatch
の呼び出しは、そのままで構いません。dispatch
の引数にアクションを渡すことは変わらないからです。
// 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
としました。
// 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
は忘れないでください。
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」Example:
サンプル003■React + TypeScript: useImmerReducer 01
https://codesandbox.io/s/react-typescript-useimmerreducer-01-40nmsi
Immer公式サイト「useImmerReducer」demo:
サンプル004■React + TypeScript: useImmerReducer 02
https://codesandbox.io/s/react-typescript-useimmerreducer-02-qmx5nh