React・仕事で使うために

stateが増えたらuseReducer

useStateが5個6個と増えて、更新の手順があちこちに散り始めたときの道具がuseReducerです。stateとその更新のしかたを1か所にまとめる書き方と、useStateのままでよい場面の線引きを扱います。

useStateは便利ですが、増えてくると散らかります。ことりずかんの一覧ページを見てみましょう。

const [keyword, setKeyword] = useState('')
const [onlyFavorites, setOnlyFavorites] = useState(false)

いまは2つなので問題ありません。ここに実務でよくある要求が来たとします。

  • 並べ替え(名前順・レア度順)を足したい
  • ページ送りを足したい
  • 検索語を変えたら、ページを1に戻したい

最後の1つが効いてきます。state同士が連動し始めたのです。

function handleKeywordChange(value) {
  setKeyword(value)
  setPage(1)        // ← 書き忘れると、2ページ目のまま検索されてバグる
}

この「一緒に変えなければいけない」が増えるほど、書き忘れが起きます

useReducer——更新のしかたを1か所に集める

useReducerは、stateと「どう変わるか」をひとまとめにする道具です。

import { useReducer } from 'react'

const initialState = {
  keyword: '',
  onlyFavorites: false,
  sort: 'name',
  page: 1,
}

function reducer(state, action) {
  switch (action.type) {
    case 'search':
      // 検索したらページは必ず1に戻す——このルールがここに閉じこめられる
      return { ...state, keyword: action.value, page: 1 }
    case 'toggleOnlyFavorites':
      return { ...state, onlyFavorites: !state.onlyFavorites, page: 1 }
    case 'changeSort':
      return { ...state, sort: action.value, page: 1 }
    case 'changePage':
      return { ...state, page: action.value }
    default:
      return state
  }
}

function BirdListPage() {
  const [state, dispatch] = useReducer(reducer, initialState)

}

使う側はこうなります。

<input
  value={state.keyword}
  onChange={(event) => dispatch({ type: 'search', value: event.target.value })}
/>

「検索した」と知らせるだけで、「ページを1に戻す」は書きません。そのルールはreducerが知っているからです。

useStateのままでよい場面

「増えたら全部reducerにする」ではありません。線引きははっきりしています

状況どちらを使うか
stateが2〜3個で、それぞれ独立しているuseState
入力欄の値のように、単純に置きかえるだけuseState
複数のstateが連動して変わるuseReducer
更新のしかたが何通りもある(追加・削除・並べ替え・リセット)useReducer
同じ更新を、離れた場所から何度も呼ぶuseReducer

reducerの中で守ること

reducerには決まりが1つあります。元のstateを書きかえないことです。

// ✕ 元を書きかえている
case 'search':
  state.keyword = action.value
  return state

// ○ 新しいオブジェクトを返す
case 'search':
  return { ...state, keyword: action.value, page: 1 }

stateの回で学んだ「直接書きかえない」が、そのまま効いています。reducerは常に新しいオブジェクトを返す——これだけ守れば大丈夫です。

Contextと組み合わせる

useReducerdispatchを1つ配るだけで済むので、Contextと組み合わせるのが定番です。

<FilterContext value={{ state, dispatch }}>

値をいくつも配る代わりに、statedispatchの2つだけを配ればよくなります。規模が大きくなったアプリで、この形はよく見かけます。

ReactContextで配らずに共有するContextの使いどころはこの回に。useReducerと組み合わせるのは、その自然な発展形です

公式ドキュメントも見てみる

ja.react.dev外部サイトstate ロジックをリデューサに抽出する – React公式の「stateロジックをリデューサに抽出する」。useStateからuseReducerへ書きかえる手順が、3ステップで整理されています

このレッスンのまとめ

  1. useState連動するstateが増えると書き忘れが起きる
  2. useReducerstateと更新のしかたを1か所にまとめる道具
  3. 登場人物はstate・action・reducer。reducerはただの関数でテストしやすい
  4. 呼ぶ側は「何が起きたか」をdispatchするだけ。更新の手順を知らなくてよい
  5. 2〜3個の独立したstateならuseStateのままでよい
  6. reducerでも元のstateを書きかえないdefaultreturn stateを忘れない
  7. 速くはならない。ただしdispatchは変わらないのでmemoした子に渡しやすい

次は、Reactに任せない部分——refでDOMを直接触る話です。

やってみよう:絞りこみをreducerにまとめる

一覧ページの検索と「お気に入りだけ」を、useReducerに移します。

  1. BirdListPage.jsxの上にinitialStatereducerを書く(コンポーネントの外)
  2. useState2つをconst [state, dispatch] = useReducer(reducer, initialState)に置きかえる
  3. 検索のonChangedispatch({ type: 'search', value: event.target.value })にする
  4. チェックボックスをdispatch({ type: 'toggleOnlyFavorites' })にする
  5. 絞りこみで使っているkeywordonlyFavoritesstate.keywordstate.onlyFavoritesに直す
const initialState = {
  keyword: '',
  onlyFavorites: false,
}

function reducer(state, action) {
  switch (action.type) {
    case 'search':
      return { ...state, keyword: action.value }
    case 'toggleOnlyFavorites':
      return { ...state, onlyFavorites: !state.onlyFavorites }
    default:
      return state
  }
}

書きかえたあと、画面の動きが1つも変わっていないことを確認してください。useReducerは見た目を変える道具ではなく、コードの置き場所を変える道具です。動きが同じなら成功です。

余力があれば、{ type: 'reset' }return initialState)を足して、「条件をクリア」ボタンを作ってみてください。1行で全部戻せるのが、まとめたことの効きめです。

よくある質問

useStateとuseReducerはどう使い分けますか?
state同士が関係なく、更新も単純ならuseStateのままで十分です。複数のstateが連動して変わる、更新のしかたが何通りもある、同じ更新を別の場所からも呼ぶ——このどれかが当てはまったらuseReducerを検討します。
reducerとは何ですか?
いまの状態とアクションを受け取って、次の状態を返す関数です。Reactの機能ではなくただの関数なので、コンポーネントの外に置けますし、単体でテストもできます。
dispatchは何をしていますか?
「こういうことが起きた」という知らせをreducerに送っています。どう更新するかはreducerが決めるので、呼び出し側は更新の手順を知らなくてよくなります。
useReducerを使うと再描画は減りますか?
減りません。目的は速さではなく、更新のしかたを1か所に集めて読みやすくすることです。dispatchは中身が変わらないので、memoした子への受け渡しでは扱いやすいという利点はあります。