概要
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
に従えということなのでしょうか。