state内のオブジェクトを更新する
stateはオブジェクトを含むあらゆる種類のJavaScriptの値を保持できます。しかし、Reactのstateに保持しているオブジェクトを直接変更してはいけません。代わりに、オブジェクトを更新したいときは、新しいオブジェクトを作成する(または既存のオブジェクトのコピーを作成する)必要があり、その後stateをそのコピーを使用するように設定します。
学習内容
- React state内のオブジェクトを正しく更新する方法
- オブジェクトを変更せずにネストされたオブジェクトを更新する方法
- イミュータビリティとは何か、そしてそれを壊さない方法
- Immerを使ってオブジェクトのコピーを繰り返し少なくする方法
ミューテーションとは?
stateにはあらゆる種類のJavaScriptの値を保存できます。
これまで、数値、文字列、ブール値を扱ってきました。これらの種類のJavaScriptの値は「イミュータブル」、つまり変更不可能または「読み取り専用」です。再レンダーをトリガーして値を置き換えることができます:
stateのxは0から5に変わりましたが、数値0自体は変更されていません。JavaScriptでは、数値、文字列、ブール値のような組み込みのプリミティブ値に対して変更を加えることはできません。
次に、state内のオブジェクトを考えてみましょう:
技術的には、オブジェクト自体の内容を変更することは可能です。これはミューテーションと呼ばれます:
しかし、React state内のオブジェクトは技術的にはミュータブルですが、それらを数値、ブール値、文字列のようにイミュータブルであるかのように扱うべきです。それらを変更するのではなく、常に置き換えるべきです。
stateを読み取り専用として扱う
言い換えれば、stateに置くJavaScriptオブジェクトはすべて読み取り専用として扱うべきです。
この例では、現在のポインタ位置を表すオブジェクトをstateに保持しています。プレビューエリア上でタッチまたはカーソルを動かすと、赤い点が移動するはずです。しかし、点は初期位置に留まったままです:
問題はこのコード部分にあります。
このコードは、positionに割り当てられたオブジェクトを前回のレンダーから変更します。しかし、state設定関数を使用しないと、Reactはオブジェクトが変更されたことを認識しません。そのため、Reactは何も応答しません。それは、食事を食べ終わった後に注文を変更しようとするようなものです。stateのミューテーションが場合によっては機能することもありますが、推奨しません。レンダー内でアクセスできるstateの値は読み取り専用として扱うべきです。
この場合に実際に再レンダーをトリガーするには、新しいオブジェクトを作成してstate設定関数に渡します:
このsetPositionによって、Reactに次のように伝えています:
- この新しいオブジェクトで
positionを置き換える - そしてこのコンポーネントを再レンダーする
プレビューエリア上でタッチまたはホバーすると、赤い点がポインタに追従する様子を確認してください:
スプレッド構文によるオブジェクトのコピー
前の例では、positionオブジェクトは常に現在のカーソル位置から新しく作成されていました。しかし多くの場合、作成中の新しいオブジェクトの一部として既存のデータを含めたいことがあります。例えば、フォームの1つのフィールドだけを更新し、他のすべてのフィールドの以前の値を保持したい場合などです。
これらの入力フィールドは、onChangeハンドラーが状態をミューテートするため動作しません:
例えば、この行は過去のレンダリングからの状態をミューテートしています:
求めている動作を得る確実な方法は、新しいオブジェクトを作成してそれをsetPersonに渡すことです。しかしここでは、変更されたフィールドは1つだけなので、既存のデータもそれにコピーしたいのです:
各プロパティを個別にコピーする必要がないように、...オブジェクトスプレッド構文を使用できます。
これでフォームが動作します!
各入力フィールドに対して個別の状態変数を宣言していないことに注目してください。大きなフォームでは、すべてのデータをオブジェクトにグループ化して保持することは、正しく更新する限り非常に便利です!
...スプレッド構文は「浅い」コピーであることに注意してください。つまり、1階層だけコピーします。これにより高速ですが、ネストされたプロパティを更新したい場合は、複数回使用する必要があることも意味します。
ネストされたオブジェクトの更新
次のようなネストされたオブジェクト構造を考えてみましょう:
もしperson.artwork.cityを更新したい場合、ミューテーションを使えばどうするかは明らかです:
しかしReactでは、stateはイミュータブルとして扱います!cityを変更するには、まず新しいartworkオブジェクト(前のオブジェクトのデータで事前に埋められたもの)を作成し、次にその新しいartworkを指す新しいpersonオブジェクトを作成する必要があります:
または、単一の関数呼び出しとして記述すると:
これは少し冗長になりますが、多くの場合に問題なく動作します:
Immerで簡潔な更新ロジックを書く
stateが深くネストされている場合は、平坦化することを検討してもよいでしょう。しかし、stateの構造を変更したくない場合は、ネストされたスプレッド構文のショートカットを好むかもしれません。Immerは、便利だがミューテーションを行う構文を使って記述し、コピーの作成を代わりに行ってくれる人気のライブラリです。Immerを使うと、あなたが書くコードは「ルールを破って」オブジェクトをミューテートしているように見えます:
しかし、通常のミューテーションとは異なり、過去の状態を上書きしません!
Immerを試すには:
- Immerを依存関係として追加するために、
npm install use-immerを実行します - 次に、
import { useState } from 'react'をに置き換えますimport { useImmer } from 'use-immer'
以下は、上記の例をImmerに変換したものです:
イベントハンドラーがどれだけ簡潔になったかに注目してください。単一のコンポーネント内でuseStateとuseImmerを好きなだけ組み合わせて使用できます。Immerは、特に状態にネストがあり、オブジェクトのコピーが繰り返しのコードにつながる場合に、更新ハンドラーを簡潔に保つ優れた方法です。
まとめ
- Reactのすべてのstateは不変として扱います。
- stateにオブジェクトを格納する場合、それを変更してもレンダーはトリガーされず、以前のレンダーの「スナップショット」内のstateが変更されてしまいます。
- オブジェクトを変更する代わりに、その新しいバージョンを作成し、stateをそれに設定することで再レンダーをトリガーします。
- オブジェクトのコピーを作成するには、
{...obj, something: 'newValue'}というオブジェクトのスプレッド構文を使用できます。 - スプレッド構文は浅いコピーです:1階層しかコピーしません。
- ネストされたオブジェクトを更新するには、更新する場所から上に向かってすべてのコピーを作成する必要があります。
- 繰り返しのコピーコードを減らすには、Immerを使用します。
Try out some challenges
Challenge 1 of 3:Fix incorrect state updates #
This form has a few bugs. Click the button that increases the score a few times. Notice that it does not increase. Then edit the first name, and notice that the score has suddenly “caught up” with your changes. Finally, edit the last name, and notice that the score has disappeared completely.
Your task is to fix all of these bugs. As you fix them, explain why each of them happens.
