useReducerの実践的な活用パターン
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 (...)
}
dispatchでactionを渡すことで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更新ロジックを一箇所にまとめられる
- 計算や変換ロジックを確実に実行できる
- 余計な関数定義を減らせる
useStateとuseReducerを適切に使い分けることで、より安全で保守性の高いコードを書けます。
参考
Discussion
※ 公式からの抜粋
「多くのイベントハンドラにまたがって state の更新コードが含まれるコンポーネントは、理解が大変になりがちです。このような場合、コンポーネントの外部に、リデューサ (reducer) と呼ばれる単一の関数を作成し、すべての state 更新ロジックを集約することができます。イベントハンドラは、ユーザの「アクション」を指定するだけでよくなるため、簡潔になります。以下ではファイルの最後にあるリデューサ関数が、各アクションに対する state の更新方法を指定しています!」
useReducerは複数のhandlerにstateの更新がまたがる際に、stateの更新ロジックを集約して管理しやすくするためのhooksなのでシンプルなstateに使用するのは用途が違うかなと思います。
useStateの存在意義がなくなります。