LoginSignup
4
8

More than 3 years have passed since last update.

React HookでオブジェクトをuseStateではなくuseReducerで管理する

Last updated at Posted at 2019-10-20

概要

setStateでオブジェクトを管理していて起きた予期せぬ動作について調べてみたので記事として残しておきます。

TL;DR

React Hookにおいて、useStateで複雑なオブジェクトを扱うのは最適解ではないのでuseReducerを使用する。

フック API リファレンスより引用

クラスコンポーネントの setState メソッドとは異なり、useState は自動的な更新オブジェクトのマージを行いません。

別の選択肢としては useReducer があり、これは複数階層の値を含んだ state オブジェクトを管理する場合にはより適しています。

useStateではプリミティブ型、またはプリミティブ型からなるオブジェクトを扱い、それ以外のオブジェクトにはuseReducerを使用する。

詳細

Next.jsdiv要素、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を使用するという結論を得ました。

少し話は逸れますが、なぜ片方のプロパティが更新され、他方は更新されないのか?という点については結論はまだ出ていません。

今回の扱うオブジェクトはbooleanJSX.Elementからなるオブジェクトです。
あくまで推測ですが、プリミティブ型とオブジェクト型が混合している場合、動作が未定義になるということかもしれません。

試しにstringbooleanの場合で試してみた場合、期待通りプロパティは全て正常に更新されました。

上記のような予期せぬ動作は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を登録し、statedispatchを生成してあげれば大丈夫です。

const [state, dispatch] : [
    State,
    React.Dispatch<boolean>
] = React.useReducer(reducer, {
    selected: false,
    element: <div onDoubleClick={() => dispatch(state.selected)}>
        {text}
    </div>
})

reducerの概念は処理の流れを決定するデザインパターンだと認識していましたが、React Hookでは複雑なオブジェクトを扱う場合はuseReducerに従えということなのでしょうか。

4
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
8