導入
第4章の TODO アプリ(レッスン12)を思い出してください。「追加」「削除」のように更新の種類が増えるほど、setItems(...) を呼ぶ場所があちこちに散らばっていきます。useReducer は「どう更新するか」を1つの関数(reducer)にまとめ、呼び出し側は「何をしたいか」を表す小さな命令(action)を送るだけにする書き方です。
説明
reducer(現在のstate, action) は「次の state」を返す関数です。dispatch(action) を呼ぶと React が reducer を実行し、その結果で state を更新します。
flowchart LR
ui["UIのボタンなど"] -->|"dispatch({ type: '...' })"| reducer["reducer(state, action)"]
reducer -->|"新しいstateを返す"| state["state"]
state -->|"再描画"| ui
同じ TODO アプリを useReducer で書き直します。
function reducer(state, action) {
switch (action.type) {
case "add":
return [...state, action.text];
case "remove":
return state.filter((_, i) => i !== action.index);
case "clear":
return [];
default:
return state;
}
}
function App() {
const [items, dispatch] = React.useReducer(reducer, ["牛乳を買う"]);
const [text, setText] = React.useState("");
function add() {
if (text.trim() === "") return;
dispatch({ type: "add", text });
setText("");
}
return (
<div style={{ fontFamily: "system-ui", maxWidth: 320 }}>
<h2>やることリスト(useReducer版)</h2>
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="やることを入力" />
<button onClick={add}>追加</button>
<button onClick={() => dispatch({ type: "clear" })}>全部消す</button>
<ul>
{items.map((item, i) => (
<li key={i}>
{item}{" "}
<button onClick={() => dispatch({ type: "remove", index: i })}>削除</button>
</li>
))}
</ul>
</div>
);
}
React.useReducer(reducer, 初期値)…[state, dispatch]のペアを返します(useStateの[値, 更新関数]に対応する形です)。dispatch({ type: "add", text })… 「何をしたいか」を表すオブジェクト(action)を送るだけ。実際に配列をどう作り直すかはreducerの中だけに書きます。- 更新ロジックが1つの関数にまとまるので、「バグがあったら reducer を見ればいい」「reducer だけをテストできる」というメリットがあります。これは後で紹介する Redux と同じ考え方です。
やってみよう
reducer に "addFront"(先頭に追加する)という action を追加し、ボタンから dispatch({ type: "addFront", text }) を呼べるようにしてみましょう。