リデューサ関数とは
コンポーネントが複雑になっていくと、state の更新を追跡するのが大変になる。
例えば、タスクの追加・編集・削除などがある場合、それぞれに対応するボタンにイベントハンドラを割り付け、セット関数によって更新するという流れになる:
export default function TaskApp() { const [tasks, setTasks] = useState(initialTasks); function handleAddTask(text) { setTasks([ ...tasks, { id: nextId++, text: text, done: false, }, ]); } function handleChangeTask(task) { setTasks( tasks.map((t) => { if (t.id === task.id) { return task; } else { return t; } }) ); } function handleDeleteTask(taskId) { setTasks(tasks.filter((t) => t.id !== taskId)); } // ...略... }
そこで使えるのがリデューサ関数で、コンポーネントの外部に関数に state ロジックを移動させることができる。
リデューサ関数への移行は次のステップで行える。
- state セットをアクションのディスパッチに置き換える
- リデューサ関数を作成する
- コンポーネントからリデューサを使用する
state セットのアクションをディスパッチに置き換える
リデューサは、state をセットして『何をするか』を指示するのではなく、イベントハンドラから『アクション』をディスパッチ(割り当て)して指定する。
ディスパッチへの置き換えは次の手順で進める:
- state をセットするロジックを全て削除
- イベントハンドラからアクションをディスパッチする
function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task, }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId, }); }
ディスパッチに渡しているオブジェクトを "アクション"と呼ぶ。慣習として、何が起こったかを説明する type
を与え、他のフィールドで追加情報を渡す。
type
はコンポーネント固有のものとする。
リデューサ関数を作成する
リデューサ関数に state のロジックを記述する。以下の手順を踏む:
- 現在の state を最初の引数として宣言
- action オブジェクトを2番目の引数として宣言
- リデューサから次の state を返す(state へのセットは React が処理する)
function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [ ...tasks, { id: action.id, text: action.text, done: false, }, ]; } case 'changed': { return tasks.map((t) => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter((t) => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } }
上記のように、リデューサ関数は state を引数として受け取るので、コンポーネントの外部で宣言可能。
コンポーネントからリデューサを使用する
React から useReducer
フックをインポートする。
useState
の呼び出しを useReducer
で置き換える:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
良いリデューサの書き方
リデューサは純粋である必要あり
同じ入力に対して常に同じ出力とし、オブジェクト配列を書き換えせずに更新する。よって、リデューサの中で動的に変化するようなロジック入れない。
各アクションは、複数データの更新でも単一のユーザ操作を記述する
個々にアクションをディスパッチするよりも、1つにまとめてディスパッチする方が効率的
直接 state をセットするのとリデューサの違いのまとめ
分類 | 説明 |
---|---|
state のセット | イベントハンドラに『何をするか(タスク)』をセットする |
リデューサ | イベントハンドラに『何をしたか(アクション)』を割り当てる |
ToDo アプリのコードを参考にすると、コード的には次のような違い:
// state を直接セットする場合は、何をするかをセットする function handleAddTask(text) { setTasks([ ...tasks, { id: nextId++, text: text, done: false, }, ]); } // reducer の場合は、アクションを割り当てる function handleAddTask(text) { dispatch({ type: 'added', // 慣習として type にアクションの説明を設定 id: nextId++, text: text, }); }
上記の場合、reducer のロジックがないため、ロジック部分をリデューサ関数内に記述する:
function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [ ...tasks, { id: action.id, text: action.text, done: false, }, ]; } default: { throw Error('Unknown action: ' + action.type); } } }
リデューサ関数では、第1引数に現在の state, 第2引数に action オブジェクトを渡し、リデューサ関数では 次の state が戻り値になる。