useReducerの活用方法を広げてみよう
useReducer
と聞いて、複雑な処理をする時に使用するAPIだと思っている方も多いのではないでしょうか。はじめは私もそう思っていました。
しかし、この記事に出会ってから新たな活用方法を見出すことができました。
実際に使ってみると、useState
と同じようにシンプルな処理にも使用できることがわかります。むしろ、useState
よりもuseReducer
の方がシンプルに書けることもあります。
今回は、useReducer
の活用方法を紹介します。
useReducer
とは
useReducer
は、ReactのHooks APIの1つです。新たなstateを返す関数を定義し、それをuseReducer
の第一引数に渡すことで、stateを更新できます。
おそらく、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 (...)
}
こんな風にdispatch
で action
を渡すことで、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
の活用方法を広げてみてください。こういう使い方あるよ、というコメントもお待ちしています。
参考
Discussion
※ 公式からの抜粋
「多くのイベントハンドラにまたがって state の更新コードが含まれるコンポーネントは、理解が大変になりがちです。このような場合、コンポーネントの外部に、リデューサ (reducer) と呼ばれる単一の関数を作成し、すべての state 更新ロジックを集約することができます。イベントハンドラは、ユーザの「アクション」を指定するだけでよくなるため、簡潔になります。以下ではファイルの最後にあるリデューサ関数が、各アクションに対する state の更新方法を指定しています!」
useReducerは複数のhandlerにstateの更新がまたがる際に、stateの更新ロジックを集約して管理しやすくするためのhooksなのでシンプルなstateに使用するのは用途が違うかなと思います。
useStateの存在意義がなくなります。