目次
useReducerとロジックの整理
useReducer とは(useStateとの関係)
useState- 「単純な状態」をサクッと管理するのに向いている
- 小さいコンポーネントではこれだけで十分
useReducer- 状態の構造が少し複雑になってきたときに、「状態更新のロジックを1か所に集約」 するための仕組み
シグネチャ:
const [state, dispatch] = useReducer(reducer, initialState)state:現在の状態dispatch:アクションを送る関数reducer:(state, action) => newStateな関数initialState:初期状態
シンプルなカウンターで感覚を掴む
まずは useState 版と useReducer 版を比べてみます。
useState版
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={() => setCount((c) => c - 1)}>-1</button>
<button onClick={() => setCount(0)}>リセット</button>
</div>
)
}useReducer版
import { useReducer } from 'react'
const initialState = { count: 0 }
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return { count: 0 }
default:
return state
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
<p>カウント: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'reset' })}>リセット</button>
</div>
)
}useReducerの場合:
- 更新ロジックは全部
reducerに集める - コンポーネント側では
- 「何をしたいか」=
dispatch({ type: 'increment' })だけ送る
- 「何をしたいか」=
この「状態更新のif/switchを1か所に集約する」のがメリットで、状態が増えてきたときにコードの見通しがかなりよくなります。
いつ useReducer を使うべきか?
判断基準:
- useReducer を検討したくなる条件
useStateが3〜4個以上あり、それぞれが関連している- 「このイベントのときは、AもBもCもまとめて更新したい」
- 更新パターンが複数あり、if/else や setState があちこちに散らばる
- まだ useState で十分なケース
- 単純なカウンター
- 小さなフォーム
- 画面ごとに状態がほぼ独立している
大事なのは、「複雑な状態を1つのオブジェクトにまとめて、更新ロジックをreducerに集めるとスッキリするか?」という視点です。
フォームを useReducer で管理する例
フォーム系は、項目数が増えるので useReducer との相性が良いです。
例:会員登録フォーム
name,email,password,plan,agreeなど複数項目- 「リセット」「まとめてエラー処理」など更新パターンが多い
import { useReducer } from 'react'
const initialState = {
name: '',
email: '',
password: '',
plan: 'free',
agree: false,
}
function reducer(state, action) {
switch (action.type) {
case 'change_field':
return {
...state,
[action.field]: action.value,
}
case 'reset':
return initialState
default:
return state
}
}
export default function RegisterForm() {
const [form, dispatch] = useReducer(reducer, initialState)
function handleChange(e) {
const { name, type, checked, value } = e.target
dispatch({
type: 'change_field',
field: name,
value: type === 'checkbox' ? checked : value,
})
}
function handleSubmit(e) {
e.preventDefault()
console.log('送信データ:', form)
// バリデーション等はここで
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>
名前:
<input
name="name"
value={form.name}
onChange={handleChange}
/>
</label>
</div>
<div>
<label>
メール:
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
/>
</label>
</div>
<div>
<label>
パスワード:
<input
name="password"
type="password"
value={form.password}
onChange={handleChange}
/>
</label>
</div>
<div>
<label>
プラン:
<select
name="plan"
value={form.plan}
onChange={handleChange}
>
<option value="free">無料</option>
<option value="pro">Pro</option>
</select>
</label>
</div>
<div>
<label>
<input
name="agree"
type="checkbox"
checked={form.agree}
onChange={handleChange}
/>
利用規約に同意する
</label>
</div>
<button type="submit">登録</button>
<button type="button" onClick={() => dispatch({ type: 'reset' })}>
リセット
</button>
</form>
)
}ここでのポイント:
formを1つのオブジェクト状態として扱い、change_fieldアクションで共通的に更新しているresetアクションで「全部初期化」を簡単に実現
もしこれを全部 useState でやると:
setName,setEmail,setPassword, …- リセット時に全部
setX('')を呼ぶ
というコードがApp全体に散らばっていきます。useReducer によって、それらのロジックが reducer 内にまとまるのがメリットです。
Todoアプリを useReducer で書き直してみる
前回やったTodoアプリも、useReducer だとこう書けます。
reducer と state
const initialTodos = []
function todoReducer(todos, action) {
switch (action.type) {
case 'add': {
return [
...todos,
{
id: action.id,
title: action.title,
done: false,
},
]
}
case 'toggle': {
return todos.map((todo) =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
)
}
case 'delete': {
return todos.filter((todo) => todo.id !== action.id)
}
default:
return todos
}
}コンポーネント側
import { useReducer, useState } from 'react'
let nextId = 1
export default function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, initialTodos)
const [text, setText] = useState('')
function handleAdd(e) {
e.preventDefault()
if (!text.trim()) return
dispatch({ type: 'add', id: nextId++, title: text.trim() })
setText('')
}
return (
<div>
<h1>Todo App (useReducer版)</h1>
<form onSubmit={handleAdd}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="タスクを入力"
/>
<button type="submit">追加</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() =>
dispatch({ type: 'toggle', id: todo.id })
}
/>
<span
style={{
textDecoration: todo.done ? 'line-through' : 'none',
}}
>
{todo.title}
</span>
</label>
<button
onClick={() => dispatch({ type: 'delete', id: todo.id })}
>
削除
</button>
</li>
))}
</ul>
</div>
)
}ここでは:
- Todoの操作(追加・トグル・削除)を全部
todoReducerに押し込めた - コンポーネント側は「どんなアクションか」を dispatch するだけ
大規模になってくると、
addtoggleeditclear_donereorder- …
など操作が増えますが、reducer の中だけ見れば「Todo一覧に何が起こりうるか」が全部わかるようになります。
useReducer + Context = 軽量Redux っぽい使い方
useReducer は Context と組み合わせると、Redux風の「グローバルな状態管理」 に発展させることもできます。
イメージ:
const TodoContext = createContext()
function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, initialTodos)
const value = { todos, dispatch }
return (
<TodoContext.Provider value={value}>
{children}
</TodoContext.Provider>
)
}
function useTodos() {
return useContext(TodoContext)
}これで、どのコンポーネントからでも:
const { todos, dispatch } = useTodos()
dispatch({ type: 'add', ... })のように扱えるようになります。
今の段階では、「useReducerで状態と更新ロジックを1か所に集められる ⇒ それをContextで配るとグローバル状態にできる」くらいがわかっていれば十分です。
まとめ
useReducerは「状態更新ロジックを1か所に集約する」ためのフックconst [state, dispatch] = useReducer(reducer, initialState)reducer(state, action) => newState
useStateが多くて「このイベントでAもBもCも更新する」ようなケースに向いている- フォームやTodoなど、「操作の種類が多い」「状態が1つのオブジェクトにまとまっている」場合と相性が良い
reducerを見るだけで「この状態に対して何が起こりうるか」が一覧できるのが利点- Context と組み合わせて「軽量Redux」的な使い方もできる
コメント