⚛️

useReducerの実践的な活用パターン

に公開
1

useReducerは複雑な処理にのみ使うものだと思っていませんか?実は、シンプルな処理でもuseStateより簡潔に書ける場合があります。

この記事では、Creative ways of using useReducerを参考に、useReducerの新しい活用方法を紹介します。

useReducerとは

ReactのHooks APIの1つで、新しいstateを返す関数(reducer)を定義してstateを更新します。

公式ドキュメント: https://react.dev/reference/react/useReducer

一般的な使い方

Reduxのreducerと同様、複雑なstateを管理する場合によく使われます。

function reducer (state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      throw new Error()
  }
}

export default function App () {
  const [state, dispatch] = useReducer(reducer, { count: 0 })

  const increment = () => dispatch({ type: 'increment' })
  const decrement = () => dispatch({ type: 'decrement' })

  return (...)
}

dispatchactionを渡すことでreducerが呼ばれ、stateが更新されます。しかし、シンプルなstateを扱う場合でもuseReducerは有効です。

パターン1: トグルボタン

ボタンを押すとON/OFFが切り替わるシンプルな機能を実装します。

useStateの場合

export default function App () {
  const [isOn, setIsOn] = useState(false)
  const toggle = () => setIsOn(!isOn)

  return (
    <button onClick={toggle}>
      {isOn ? 'ON' : 'OFF'}
    </button>
  )
}

useReducerの場合

export default function App () {
  const [isOn, toggle] = useReducer((v) => !v, false)

  return (
    <button onClick={toggle}>
      {isOn ? 'ON' : 'OFF'}
    </button>
  )
}

メリット: toggle関数を別途定義する必要がなく、そのままonClickに渡せます。

パターン2: 計算ロジックの一元管理

入力した価格に消費税を加算して表示する例です。

const addTax = (price) => price * 1.08

useStateの場合

export default function App () {
  const [price, setPrice] = useState(0)
  const [taxIncludedPrice, setTaxIncludedPrice] = useState(0)

  const handleChange = (e) => {
    const value = e.target.value
    setPrice(value)

    if (value > 0) {
      const taxIncludedPrice = addTax(value)
      setTaxIncludedPrice(taxIncludedPrice)
    }
  }

  return (
    <div>
      <input type="number" value={price} onChange={handleChange} />
      <p>{taxIncludedPrice}</p>
    </div>
  )
}

問題点: setTaxIncludedPriceを呼ぶ前にaddTaxを呼び忘れるとバグになります。

useReducerの場合

export default function App () {
  const [price, setPrice] = useState(0)
  const [taxIncludedPrice, setTaxIncludedPrice] = useReducer((_, price) => addTax(price), 0)

  const handleChange = (e) => {
    const value = e.target.value
    setPrice(value)

    if (value > 0) {
      setTaxIncludedPrice(value)
    }
  }

  return (
    <div>
      <input type="number" value={price} onChange={handleChange} />
      <p>{taxIncludedPrice}</p>
    </div>
  )
}

メリット: addTaxが必ず実行されるため、計算漏れがなくなります。

パターン3: フォームのステート管理

複数のフィールドを持つフォームの状態管理を安全に行います。

useStateの場合

const initialState = {
  name: '',
  email: '',
}

export default function App () {
  const [user, setUser] = useState(initialState)

  const handleChange = (e) => {
    const { name, value } = e.target
    setUser((prevState) => ({ ...prevState, [name]: value }))
  }

  return (
    <div>
      <input type="text" name="name" value={user.name} onChange={handleChange} />
      <input type="email" name="email" value={user.email} onChange={handleChange} />
    </div>
  )
}

問題点: setStateの度にスプレッド構文で展開する処理を書き忘れると、stateが正しく更新されません。

useReducerの場合

const initialState = {
  name: '',
  email: '',
}

export default function App () {
  const [user, setUser] = useReducer(
    (currentState, update) => ({ ...currentState, ...update }),
    initialState
  )

  const handleChange = (e) => {
    const { name, value } = e.target
    setUser({ [name]: value })
  }

  return (
    <div>
      <input type="text" name="name" value={user.name} onChange={handleChange} />
      <input type="email" name="email" value={user.email} onChange={handleChange} />
    </div>
  )
}

メリット: reducer内で必ずスプレッド構文による展開が実行されるため、stateの更新漏れが発生しません。

まとめ

useReducerは複雑なstate管理だけでなく、シンプルな処理でも有効です。

主な利点:

  • state更新ロジックを一箇所にまとめられる
  • 計算や変換ロジックを確実に実行できる
  • 余計な関数定義を減らせる

useStateuseReducerを適切に使い分けることで、より安全で保守性の高いコードを書けます。

参考

https://www.code-insights.dev/posts/creative-ways-of-using-usereducer

Discussion

fallfall

※ 公式からの抜粋
「多くのイベントハンドラにまたがって state の更新コードが含まれるコンポーネントは、理解が大変になりがちです。このような場合、コンポーネントの外部に、リデューサ (reducer) と呼ばれる単一の関数を作成し、すべての state 更新ロジックを集約することができます。イベントハンドラは、ユーザの「アクション」を指定するだけでよくなるため、簡潔になります。以下ではファイルの最後にあるリデューサ関数が、各アクションに対する state の更新方法を指定しています!」

useReducerは複数のhandlerにstateの更新がまたがる際に、stateの更新ロジックを集約して管理しやすくするためのhooksなのでシンプルなstateに使用するのは用途が違うかなと思います。
useStateの存在意義がなくなります。