👌

useStateで状態として、オブジェクト・配列を持つ場合

2023/03/11に公開

状態としてオブジェクトと配列を持つ場合の注意点

React では、状態で保持しているオブジェクト・配列を直接変更するべきではありません。
React の配列・オブジェクトは読み取り専用として扱う必要があります。
オブジェクトや配列を操作変更するのではなく、新しいオブジェクトや配列を作成して、状態に渡す必要があります。

オブジェクトの例

下の CodeSandbox のコードの中でカーソルのポイントが動くたびに呼び出されるイベントハンドラーがあります。

onPointerMove={e => {
  position.x = e.clientX;
  position.y = e.clientY;
}}

上記コードでは、オブジェクトの中身を変更しようとしています。
しかし、状態設定関数を使用しないと、React はオブジェクトが変更されたことを認識できません。

onPointerMove={e => {
  setPosition({
    x: e.clientX,
    y: e.clientY
  });
}}

上記のように状態設定関数を使用すると、想定通りの動作になります。
動きとしては、

  • setPositionに新しいオブジェクトを入れて、再レンダリングを行う
  • positionで新しいオブジェクトが生成されるので、画面上の表示も切り替わる

配列の例

オブジェクトと基本同じで、配列を直接編集できません。
あくまで、新しい配列を生成して状態設定関数に代入して新しい配列を持った状態を生成します。
下記表を参考に推奨される方法で配列を作成コピーしてください。

回避 (配列を変更) 好む (新しい配列を返す)
追加 push、unshift concat、[...arr]展開構文
削除する pop、shift、splice filter、slice
交換する splice、arr[i] = ...代入 map
並べ替え reverse、sort 最初に配列をコピーします
  • 配列への挿入
    スプレッド構文と、map() や filter() のような非ミューテーション メソッドだけではできないことがいくつかあります。
    たとえば、配列を反転または並べ替えたい場合があります。 JavaScript の reverse() および sort() メソッドは元の配列を変更しているため、直接使用することはできません。
    ただし、最初に配列をコピーしてから、変更を加えることができます。
import { useState } from "react";

let nextId = 3;
const initialList = [
  { id: 0, title: "Big Bellies" },
  { id: 1, title: "Lunar Landscape" },
  { id: 2, title: "Terracotta Army" },
];

export default function List() {
  const [list, setList] = useState(initialList);

  function handleClick() {
    const nextList = [...list];
    nextList.reverse();
    setList(nextList);
  }

  return (
    <>
      <button onClick={handleClick}>Reverse</button>
      <ul>
        {list.map((artwork) => (
          <li key={artwork.id}>{artwork.title}</li>
        ))}
      </ul>
    </>
  );
}

コピーができたので、nextList.reverse()nextList.sort() などの変更メソッドを使用したり、個々の項目に nextList[0] = "something" を割り当てたりすることもできます。
ただし、配列をコピーしても、その中の既存のアイテムを直接変更することはできません。
これは、コピーが浅いためです。新しい配列には、元の配列と同じ項目が含まれます。したがって、コピーされた配列内のオブジェクトを変更すると、既存の状態が変更されます。たとえば、このようなコードは問題です。

const nextList = [...list];
nextList[0].seen = true; // Problem: mutates list[0]
setList(nextList);

次のセクションで解決方法を見ていきましょう

https://beta.reactjs.org/learn/updating-arrays-in-state

配列内のオブジェクトの例

オブジェクトは実際には配列の「内部」に配置されているわけではありません。
コードでは「内部」にあるように見えるかもしれませんが、配列内の各オブジェクトは個別の値であり、配列が「ポイント」します。
これが、list[0] のようなネストされたフィールドを変更するときに注意する必要がある理由です。別の人のアートワーク リストが、配列の同じ要素を指している可能性があります。
ネストされた状態を更新するときは、更新したいポイントから最上位までコピーを作成する必要があります。これがどのように機能するか見てみましょう。

const myNextList = [...myList];
const artwork = myNextList.find((a) => a.id === artworkId);
artwork.seen = nextSeen; // Problem: mutates an existing item
setMyList(myNextList);

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

  • map を使用して、古いアイテムを突然変異なしで更新されたバージョンに置き換えることができます
setMyList(myList.map(artwork => {
  if (artwork.id === artworkId) {
    // Create a *new* object with changes
    return { ...artwork, seen: nextSeen };
  } else {
    // No changes
    return artwork;
  }
});

ここで、... は、オブジェクトのコピーを作成するために使用されるオブジェクト スプレッド構文です。このアプローチでは、既存の状態項目は変更されておらず、バグは修正されています。

GitHubで編集を提案

Discussion