概要
setStateでオブジェクトを管理していて起きた予期せぬ動作について調べてみたので記事として残しておきます。
TL;DR
React Hookにおいて、useStateで複雑なオブジェクトを扱うのは最適解ではないのでuseReducerを使用する。
フック API リファレンスより引用
クラスコンポーネントの setState メソッドとは異なり、useState は自動的な更新オブジェクトのマージを行いません。
別の選択肢としては useReducer があり、これは複数階層の値を含んだ state オブジェクトを管理する場合にはより適しています。
useStateではプリミティブ型、またはプリミティブ型からなるオブジェクトを扱い、それ以外のオブジェクトにはuseReducerを使用する。
詳細
Next.jsでdiv要素、input要素にダブルクリックでトグルさせるコンポーネントを作成していました。
コンポーネントの設計として下記の様なステートを
type ElementState = {
selected: boolean
element: JSX.Element
}
const [elementState, setElementState] : [
ElementState,
React.Dispatch<React.SetStateAction<ElementState>>
] = React.useState<ElementState>({
selected: false,
element: <div onDoubleClick={onDoubleClick}>
{text}
</div>
})
ダブルクリックイベントハンドラでトグルしたかったのですが
const onDoubleClick = () => {
if (elementState.selected) {
setElementState({
selected: false,
element: <div onDoubleClick={onDoubleClick}>
{text}
</div>
})
} else {
setElementState({
selected: true,
element: <input
onDoubleClick={onDoubleClick}
defaultValue={text}
/>
})
}
}
elementState.elementは更新され、elementState.selectedが更新されずトグルが実現できませんでした。
リンク1を参照した所useStateでは複雑なオブジェクトを更新するのには向かず、それらを処理したい場合にはuseReducerを使用するという結論を得ました。
少し話は逸れますが、なぜ片方のプロパティが更新され、他方は更新されないのか?という点については結論はまだ出ていません。
今回の扱うオブジェクトはbooleanとJSX.Elementからなるオブジェクトです。
あくまで推測ですが、プリミティブ型とオブジェクト型が混合している場合、動作が未定義になるということかもしれません。
試しにstring、booleanの場合で試してみた場合、期待通りプロパティは全て正常に更新されました。
上記のような予期せぬ動作はuseReducerを使用することで回避できます。
まずはreducerを定義し
interface State {
element: JSX.Element
selected: boolean
}
const reducer = (state: State, selected: boolean): State => {
if (selected) {
return {
selected: false,
element: <div onDoubleClick={() => dispatch(false)}>
{text}
</div>
}
} else {
return {
selected: true,
element: <input
onDoubleClick={() => dispatch(true)}
defaultValue={text}
/>
}
}
}
reducerを登録し、stateとdispatchを生成してあげれば大丈夫です。
const [state, dispatch] : [
State,
React.Dispatch<boolean>
] = React.useReducer(reducer, {
selected: false,
element: <div onDoubleClick={() => dispatch(state.selected)}>
{text}
</div>
})
reducerの概念は処理の流れを決定するデザインパターンだと認識していましたが、React Hookでは複雑なオブジェクトを扱う場合はuseReducerに従えということなのでしょうか。