[React講座] useReducerとロジックの整理

当ページのリンクには広告が含まれています。
目次

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 するだけ

大規模になってくると、

  • add
  • toggle
  • edit
  • clear_done
  • reorder

など操作が増えますが、reducer の中だけ見れば「Todo一覧に何が起こりうるか」が全部わかるようになります。

useReducer + Context = 軽量Redux っぽい使い方

useReducerContext と組み合わせると、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」的な使い方もできる

<<前へ(コンテキストとグローバル状態)

>>次へ(カスタムフックとロジックの再利用)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次