👨‍💻

useReducerの活用方法を広げてみよう

2023/06/19に公開
1

useReducerと聞いて、複雑な処理をする時に使用するAPIだと思っている方も多いのではないでしょうか。はじめは私もそう思っていました。
しかし、この記事に出会ってから新たな活用方法を見出すことができました。

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

実際に使ってみると、useStateと同じようにシンプルな処理にも使用できることがわかります。むしろ、useStateよりもuseReducerの方がシンプルに書けることもあります。

今回は、useReducerの活用方法を紹介します。

useReducerとは

useReducerは、ReactのHooks APIの1つです。新たなstateを返す関数を定義し、それをuseReducerの第一引数に渡すことで、stateを更新できます。

https://react.dev/reference/react/useReducer

おそらく、Reduxのreducerと同じようなイメージを持っている方が多いです。実際、Reduxのreducerと同じような使い方ができます。以下のようなコードです。

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を扱う時には、このような使い方をすることが多いです。
ですが、単純なstateを扱う時にも、useReducerを使うことで、コードをシンプルに書くことができます。

シンプルなトグルボタン

例えばトグルボタンを作りましょう。ボタンを押すとONとOFFが切り替わるシンプルな機能です。

useState

useStateを使うと以下のようなコードになります。

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

  const toggle = () => setIsOn(!isOn)

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

toggle関数を定義してその中でsetIsOnを呼び出しています。

useReducer

useReducerを使うと、以下のように書くことができます。

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

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

toggle関数をそのままonClickに渡しています。useReducerを使うことで、toggle関数を定義する必要がなくなります。

ステートに制限を加える

入力した値段に消費税込みの値段を表示させる場合を考えてみます。以下の関数は、入力した値段に消費税を加えて返す関数です。

const addTax = (price) => price * 1.08

useState

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関数を呼び出す必要があります。
もし、setTaxIncludedPriceの前にaddtax関数を呼び出す処理を書き忘れてしまった場合、税込の値が正しく表示されないというバグが発生します。

useReducer

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>
  )
}

useReducerの第一引数に渡している関数でaddTaxを使用して税込金額を算出します。handleChange関数の中でaddTax関数を呼び出す必要がなくなります。

これでaddTax関数を呼び出す場所が一箇所になり、確実に税込計算がされるようになりました。

フォームステート管理

フォームのステート管理にもuseReducerを使用すれば、コードをシンプルで安全に書くことができます。

useState

まずは、フォームのステート管理に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>
  )
}

useStateを使用すると、フォームのステートを1つのオブジェクトにまとめる必要があります。またhandleChange関数の中で、setStateを呼び出すたびにprevStateをスプレッド構文で展開して、新しいオブジェクトを作成する必要があります。
この場合、setStateを呼び出すたびに、新しいオブジェクトを作成する処理を書き忘れてしまった場合、stateが正しく更新されないというバグが発生します。

useReducer

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>
  )
}

useReducerを使用すると、useReducerの第一引数に渡している関数でprevStateをスプレッド構文で展開して、新しいオブジェクトを作成する処理を書く必要がなくなります。
必ずprevStateをスプレッド構文で展開して、新しいオブジェクトを作成する処理が実行されるようになります。

さいごに

useReducerを使用することで以下の恩恵を得ることがわかりました。

  • 第一引数に渡す関数で、stateの更新処理を一箇所にまとめることができる
  • シンプルなstateの更新処理を別に定義する必要がなくなる

ぜひ、useReducerの活用方法を広げてみてください。こういう使い方あるよ、というコメントもお待ちしています。

参考

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

Discussion

fallfall

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

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