Help us understand the problem. What is going on with this article?

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

概要

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした