【React修行日記】useReducerと型定義
学習の目的
- useReducerの基本について理解する
- useState と useReducer の違いを把握する
- 型定義を使った状態管理のメリットを学ぶ
- 複雑な状態更新を整理してTodoリストを実装できるようになる
useReducerとは
useReducerは、Reactで状態管理を行うためのフックのひとつ。useState
と同じく、コンポーネントの状態を保持し、更新するために使う。
useState
は状態だけを管理するのに対して、useReducer
は状態と状態を操作するための手段(関数)の両方を管理する。
シンプルな状態管理であればuseState
が直感的で扱いやすく、複雑な状態更新や複数の操作が関わる場合にはuseReducer
を使うことで、状態更新のロジックを整理しやすくなる。
import { useReducer } from "react";
const [state, dispatch] = useReducer(reducer, initialArg, init?)
-
state
- 現在の状態を表す変数
- useState でいうところの状態変数と同じ
-
dispatch
- 状態を更新するための関数
-
dispatch({ type: "ACTION_TYPE", ...payload })
のようにactionを渡す
-
reducer
- 状態をどう更新するかを決める関数
- signature は基本的にこう:
(state, action) => newState
-
state
:現在の状態 -
action
:dispatch で渡された命令 -
newState
:更新後の状態を返す
-
-
initialArg
- 初期状態を指定
- 配列やオブジェクトなど、管理したい状態の形に合わせる
-
init?
(オプション)- 初期状態を加工して設定したい場合に使用
useStateとuseReducerの違い(公式より)
-
コード量
- useState:単純な場合はコード少なめ
- useReducer:リデューサ関数と dispatch が必要 → 初めは多め。でも複数イベントで同じ更新がある場合は整理できる
-
可読性
- シンプルな状態更新 → useStateが読みやすい
- 複雑な状態更新 → useReducerで「何を起こすか」と「どう変えるか」を分けられる
-
デバッグ
- useReducerならリデューサ内にログを入れて「どのアクションで何が起きたか」が追いやすい
-
テスト
- リデューサは純関数だからコンポーネントに依存せず単体テストが簡単
-
使い分け
- 好みや状況次第で使い分けOK
- 複雑な状態更新がある場合に useReducer 推奨
- 1つのコンポーネントで両方使うことも可能
useReducer + 型定義で作るTodoリスト
今回は、useReducerと型定義を活用して簡単なTodoリストを作ってみた...!
【目的】
単純なuseStateでは管理が複雑になる状態を、reducerにまとめることでコードの可読性と保守性を高める
Todo 型と Action 型の定義
type Todo = {
id: number;
title: string;
is_done: boolean;
};
type Action =
| { type: "ADD"; title: string }
| { type: "TOGGLE"; id: number }
| { type: "DELETE"; id: number };
状態(state)がどういう形かとどんなアクションで更新するかを明確にする。
reducer関数
function reducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case "ADD":
return [
...state,
{ id: Date.now(), title: action.title, is_done: false },
];
case "TOGGLE":
return state.map((todo) =>
todo.id === action.id ? { ...todo, is_done: !todo.is_done } : todo
);
case "DELETE":
return state.filter((todo) => todo.id !== action.id);
default:
return state;
}
}
reducerで「stateの更新ルール」をまとめる。
今回のTodoリストでは、ADD
、TOGGLE
、DELETE
の3つの操作を処理。
ここで、先ほどの型定義が効いてくる。
ADD
ではtitle
、TOGGLE
ではid
が必要になるため、それぞれの型を定義しておくことで、reducerに渡されるactionが必ず正しい形を持つことを保証できる。
また、reducerでは状態は不変(immutable)に扱う必要がある。
状態を直接変更するとReactの再レンダリングや副作用の管理に問題が出る可能性があるため、
例えばTOGGLE
では直接stateを書き換えるのではなく、mapを使用して新しい配列を返している。
case "TOGGLE":
return state.map((todo) =>
todo.id === action.id ? { ...todo, is_done: !todo.is_done } : todo
);
コンポーネント全体
export default function Todo() {
const [todos, dispatch] = useReducer(reducer, []);
const [input, setInput] = useState("");
const handleAdd = () => {
if (!input.trim()) return;
dispatch({ type: "ADD", title: input });
setInput("");
};
const handleToggle = (id: number) => {
dispatch({ type: "TOGGLE", id });
};
const handleDelete = (id: number) => {
dispatch({ type: "DELETE", id });
};
return (
<div className="flex flex-col items-center justify-center gap-8">
<h1>TODOリスト</h1>
<div className="flex items-center gap-4 w-full">
<Input value={input} onChange={(e) => setInput(e.target.value)} />
<Button onClick={handleAdd}>追加</Button>
</div>
<ul className="flex flex-col gap-4 w-full">
{todos.map((todo) => (
<li key={todo.id} className="flex items-center g-4 justify-between">
<div className="flex items-center gap-3">
<Checkbox
id={todo.title}
checked={todo.is_done}
onCheckedChange={() => handleToggle(todo.id)}
/>
<Label
className={todo.is_done ? "line-through text-gray-500" : ""}
htmlFor={todo.title}
>
{todo.title}
</Label>
</div>
<Button
size={"sm"}
variant={"secondary"}
onClick={() => handleDelete(todo.id)}
>
削除
</Button>
</li>
))}
</ul>
</div>
);
}
※shadcn/uiを使用
各ハンドラはstate更新のルールをactionとしてreducerに渡す役割がある。
入力欄の文字列は単純なのでuseState
で管理。
補足: 空文字入力の処理
useReducerとは関係ないが、handleAdd
の以下の部分について
if (!input.trim()) return;
空文字はJavaScriptではfalse
として扱われるため、!input.trim()
で空文字の時は真偽値をひっくり返してtrue
にし、処理を中断している。
まとめ
-
useReducer
はuseState
とよく似ているが、複雑な操作を伴う場合に有効 - 状態と更新ロジックをreducerにまとめることでコードが整理され、可読性・保守性が向上する
- 型定義を使うことで、actionの形やstateの構造を型安全に管理できる
参考
Discussion