React: useStateでオブジェクトの配列の値を更新するときは新しいオブジェクトの新しい配列を作ろうという話

ReactのuseStateでハマったのでメモ。

## 要約
タイトル長いな。
- useStateでオブジェクトの配列を管理する際に思ったように値が更新できなくてハマった
- useStateで配列を更新するときは新しい配列を作らなくてはいけない
- useStateでオブジェクトを更新するときは新しいオブジェクトの配列を作らなくてはいけない
- useStateでオブジェクトの配列を更新するときは**新しいオブジェクトの新しい配列**を作らなくてはいけない

## サンプル
こんなTodoListを作ってchangeを押すとtrue/falseが入れ替わるようにしたい。
![スクリーンショット 2021-08-28 20.00.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677011/11980b91-bbd0-56e9-7875-d8f9ec6fcf3a.png)

## ダメな例その1
changeを押しても何も変わらない。
https://codesandbox.io/s/intelligent-joana-u4298?file=/src/App.js

```jsx
import { useState } from "react";

export default function App() {
  let [items, updateItems] = useState([
    { name: "item 1", done: false },
    { name: "item 2", done: true },
    { name: "item 3", done: false }
  ]);

  return (
    <div>
      <h2>Todo list</h2>
      <ul>
        {items.map((item, idx) => {
          return (
            <li key={idx}>
              <span>{`${item.name} ${item.done} `}</span>
              <button
                onClick={() => {
                  updateItems((oldItems) => {
                    oldItems[idx].done = !oldItems[idx].done;
                    return oldItems;
                  });
                }}
              >
                change
              </button>
            </li>
          );
        })}
      </ul>
    </div>
  );
}
```

## ダメな例その2：新しい配列を作ってみる
なるほど、どうやら新しい配列を作らないとReactに変更が伝わらずupdateが走らないらしい。
https://qiita.com/10mi8o/items/896df09ad89e41d48bac

というわけで新しい配列を作ってみる。
https://codesandbox.io/s/agitated-joana-4kj4s?file=/src/App.js

```jsx
              <button
                onClick={() => {
                  updateItems((oldItems) => {
                    oldItems[idx].done = !oldItems[idx].done;
                    return [...oldItems];  // 新しい配列
                  });
                }}
              >
                change
              </button>
```

しかし、最初の1回のみ変わるもあとは動かない...

## ダメな例その3：新しいオブジェクトを作ってみる
オブジェクトの方を変更しないといけないのかと思い今度は配列内部のオブジェクトを変更することにする
https://codesandbox.io/s/sweet-visvesvaraya-7yr4s?file=/src/App.js

```jsx
              <button
                onClick={() => {
                  updateItems((oldItems) => {
                    // 新しいオブジェクトを作成
                    oldItems[idx] = {
                      ...oldItems[idx],
                      done: !oldItems[idx].done
                    };
                    return oldItems;
                  });
                }}
              >
                change
              </button>
```

けど動かない。

## うまくいった例：新しいオブジェクトを作って新しい配列に突っ込む
これで思った通りに動いた。

https://codesandbox.io/s/bitter-bird-mqge7?file=/src/App.js

```jsx
              <button
                onClick={() => {
                  updateItems((oldItems) => {
                    return oldItems.map((oldItem, oldIdx) => {
                      if (oldIdx === idx) {
                        return { ...oldItem, done: !oldItem.done };
                      }
                      return oldItem;
                    });
                  });
                }}
              >
                change
              </button>
```

## なんでうまくいかなかったのか
React内部では`Object.is`を使って比較しているらしい。
https://ja.reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/is#description

Object.isの結果を表示してみると以下のようになった。

```jsx
              <button
                onClick={() => {
                  updateItems((oldItems) => {
                    console.log(
                      "新しい配列の中に古いオブジェクト:",
                      Object.is(items, [...oldItems]),
                      Object.is(items[idx], [...oldItems][idx])
                    ); // false, true
                    oldItems[idx] = {
                      ...oldItems[idx],
                      done: !oldItems[idx].done
                    };
                    console.log(
                      "古い配列の中に新しいオブジェクト",
                      Object.is(items, oldItems),
                      Object.is(items[idx], oldItems[idx])
                    ); // true, true

                    const newItems = oldItems.map((oldItem, oldIdx) => {
                      if (oldIdx === idx) {
                        return { ...oldItem, done: !oldItem.done };
                      }
                      return oldItem;
                    });
                    console.log(
                      "新しい配列の中に新しいオブジェクト",
                      Object.is(items, newItems),
                      Object.is(items[idx], newItems[idx])
                    ); // false, false

                    return newItems;
                  });
                }}
              >
                change
              </button>
```

配列の中のオブジェクトを内部でさらにどう比較しているかまでは調べられてないが、配列自体もオブジェクト自体もObject.isがfalseにならないといけないっぽい。

理解に間違い、より良い書き方などあれば教えてください🙇‍♂️