v19.2Latest

state内の配列の更新

JavaScriptでは配列は可変ですが、stateに格納する際は不変として扱うべきです。オブジェクトと同様に、stateに保存された配列を更新したい場合は、新しい配列を作成(または既存の配列のコピーを作成)し、その新しい配列を使用するようにstateを設定する必要があります。

学習内容
  • React state内の配列に対してアイテムを追加、削除、変更する方法
  • 配列内のオブジェクトを更新する方法
  • Immerを使用して配列のコピーを繰り返し少なくする方法

ミューテーションなしで配列を更新する

JavaScriptでは、配列は単なるオブジェクトの一種です。オブジェクトと同様にReact state内の配列は読み取り専用として扱うべきです。これは、arr[0] = 'bird'のように配列内のアイテムを再代入したり、push()pop()のように配列をミューテートするメソッドを使用したりすべきではないことを意味します。

代わりに、配列を更新するたびに、state設定関数に新しい配列を渡す必要があります。そのためには、state内の元の配列から、filter()map()のような非ミューテーティングメソッドを呼び出して新しい配列を作成します。その後、stateをその結果の新しい配列に設定できます。

以下は一般的な配列操作の参照表です。React state内の配列を扱う場合、左列のメソッドは避け、代わりに右列のメソッドを優先する必要があります:

避けるべきもの(配列をミューテートする)優先すべきもの(新しい配列を返す)
追加pushunshiftconcat[...arr]スプレッド構文(
削除popshiftsplicefilterslice
置換splicearr[i] = ... 代入map
ソートreversesortまず配列をコピーする(

または、Immerを使用することもできます。これにより、両方の列のメソッドを使用できます。

落とし穴

残念ながら、slicespliceは名前が似ていますが、非常に異なります:

  • sliceは配列またはその一部をコピーできます。
  • spliceは配列をミューテートします(アイテムを挿入または削除するため)。

Reactでは、state内のオブジェクトや配列をミューテートしたくないため、slicepなし!)を頻繁に使用することになります。オブジェクトの更新では、ミューテーションとは何か、そしてなぜstateに対して推奨されないのかを説明しています。

配列への追加

push()は配列をミューテートしますが、これは望ましくありません:

代わりに、既存のアイテム末尾の新しいアイテムを含む新しい配列を作成してください。これを行う方法は複数ありますが、最も簡単なのは...配列スプレッド構文を使用することです:

これで正しく動作します:

配列スプレッド構文では、新しいアイテムを元の前に配置することで、先頭に追加することもできます:...artists

このように、スプレッド構文は配列の末尾に追加するpush()と、配列の先頭に追加するunshift()の両方の役割を果たせます。上のサンドボックスで試してみてください!

配列から削除する

配列からアイテムを削除する最も簡単な方法は、そのアイテムをフィルタリングして除外することです。つまり、そのアイテムを含まない新しい配列を生成します。これを行うには、例えばfilterメソッドを使用します:

「Delete」ボタンを数回クリックし、そのクリックハンドラーを見てください。

ここで、artists.filter(a => a.id !== artist.id)は「IDがartistsからなる配列を作成する」ことを意味しますartist.idと異なるfilterは元の配列を変更しないことに注意してください。

配列を変換する

配列の一部またはすべてのアイテムを変更したい場合は、map()を使用して新しい配列を作成できます。mapに渡す関数は、各アイテムのデータやインデックス(またはその両方)に基づいて、そのアイテムに対して何を行うかを決定できます。

この例では、配列に2つの円と1つの正方形の座標が保持されています。ボタンを押すと、円だけを50ピクセル下に移動します。これはmap()を使用して新しいデータ配列を生成することで行います:

配列内のアイテムを置き換える

配列内の1つまたは複数のアイテムを置き換えたい場合は特に一般的です。arr[0] = 'bird'のような代入は元の配列を変更するため、代わりにmapを使用する必要があります。

アイテムを置き換えるには、mapで新しい配列を作成します。map呼び出しの中で、2番目の引数としてアイテムのインデックスを受け取ります。これを使用して、元のアイテム(最初の引数)を返すか、それ以外のものを返すかを決定します:

配列への挿入

時には、先頭でも末尾でもない特定の位置にアイテムを挿入したい場合があります。これを行うには、...配列スプレッド構文とslice()メソッドを組み合わせて使用できます。slice()メソッドは、配列の「スライス」を切り取ることができます。アイテムを挿入するには、挿入点より前のスライス、新しいアイテム、そして元の配列の残りの部分を展開した配列を作成します。

この例では、Insertボタンは常にインデックス1に挿入します:

配列へのその他の変更

スプレッド構文やmap()filter()のような非破壊的メソッドだけではできないこともあります。例えば、配列を逆順にしたりソートしたりしたい場合です。JavaScriptのreverse()およびsort()メソッドは元の配列を破壊的に変更するため、直接使用することはできません。

しかし、まず配列をコピーしてから、そのコピーに対して変更を加えることができます。

例:

ここでは、[...list]スプレッド構文を使用して、まず元の配列のコピーを作成します。コピーができたら、nextList.reverse()nextList.sort()のような破壊的メソッドを使用したり、nextList[0] = "something"で個々のアイテムを代入したりできます。

しかし、配列をコピーしても、その中にある既存のアイテムを直接変更することはできません。内部のアイテムです。これは、コピーが浅い(シャロー)ためです。新しい配列には元の配列と同じアイテムが含まれます。そのため、コピーした配列内のオブジェクトを変更すると、既存の状態を破壊的に変更することになります。例えば、次のようなコードは問題です。

nextListlistは2つの異なる配列ですが、nextList[0]list[0]は同じオブジェクトを指しています。したがって、nextList[0].seenを変更すると、list[0].seenも変更されます。これは状態の破壊的変更であり、避けるべきです!この問題は、ネストされたJavaScriptオブジェクトの更新と同様の方法で解決できます。つまり、変更したい個々のアイテムを破壊的に変更するのではなく、コピーします。その方法は以下の通りです。

配列内のオブジェクトの更新

オブジェクトは、配列の「中」に実際には存在しません。コード上では「中」にあるように見えますが、配列内の各オブジェクトは個別の値であり、配列がそれを「指し示している」だけです。そのため、list[0]のようなネストされたフィールドを変更する際には注意が必要です。他の人のアートワークリストが、同じ配列要素を指している可能性があります!

ネストされた状態を更新する場合、更新したいポイントからトップレベルまで、すべてのレベルでコピーを作成する必要があります。これがどのように機能するか見てみましょう。

この例では、2つの独立したアートワークリストが同じ初期状態を持っています。これらは独立しているはずですが、破壊的変更により状態が誤って共有され、一方のリストでチェックボックスをオンにすると、もう一方のリストにも影響を与えます:

問題は次のようなコードにあります:

配列myNextList自体は新しいものですが、その要素自体は元のmyList配列のものと同じです。したがって、artwork.seenを変更すると、元のアートワークアイテムが変更されます。そのアートワークアイテムはyourListにも含まれているため、バグが発生します。このようなバグは考えるのが難しい場合がありますが、幸いなことに、状態の変更を避ければ解消されます。

変更を伴わずに古いアイテムを更新されたバージョンに置き換えるには、mapを使用できます。

ここで、...はオブジェクトのスプレッド構文で、オブジェクトのコピーを作成するために使用されます。

このアプローチでは、既存の状態アイテムは一切変更されず、バグは修正されます:

一般的に、作成したばかりのオブジェクトのみを変更すべきです。新しいアートワークを挿入する場合はそれを変更できますが、すでに状態にあるものを扱う場合はコピーを作成する必要があります。

Immerを使用して簡潔な更新ロジックを書く

変更を伴わずにネストされた配列を更新することは、少し繰り返しが多くなる場合があります。オブジェクトの場合と同様に:

  • 一般的に、状態を2、3レベル以上深く更新する必要はありません。状態オブジェクトが非常に深い場合は、それらを平坦になるように再構築することを検討してください。
  • 状態構造を変更したくない場合は、Immerを使用することをお勧めします。これにより、便利だが変更を伴う構文を使用して記述でき、コピーの作成を代行してくれます。

以下は、Immerを使用して書き直したArt Bucket Listの例です:

Immerを使用すると、以下のようなミューテーションはartwork.seen = nextSeen問題ありません:

これは、元の状態を変更しているのではなく、Immerが提供する特別なdraftオブジェクトを変更しているからです。同様に、push()pop()のようなミューテーションメソッドをdraftの内容に適用できます。

内部的には、Immerは常に、draftに対して行った変更に基づいて、次の状態を一から構築します。これにより、イベントハンドラーは状態を変更することなく非常に簡潔に保たれます。

まとめ

  • 配列を状態に格納できますが、変更することはできません。
  • 配列を変更する代わりに、その新しいバージョンを作成し、状態をそれに更新します。
  • 配列スプレッド構文[...arr, newItem]を使用して、新しいアイテムを含む配列を作成できます。
  • filter()map()を使用して、フィルタリングまたは変換されたアイテムを含む新しい配列を作成できます。
  • Immerを使用してコードを簡潔に保つことができます。

Try out some challenges

Challenge 1 of 4:Update an item in the shopping cart #

Fill in the handleIncreaseClick logic so that pressing ”+” increases the corresponding number: