useContextに意図しない更新が発生する

# はじめに
タイトルのような事象に出会いました。
解決に結構時間かかってしまったのですが、原因は javascript の基本知識が足りないためでした。
React で最小限の状況を再現しながら検証したいと思います。（CodeSandbox 使いました）

# 事象再現

### 最低限の検証用ソース
```App.tsx
import { useState } from "react";
import Child from "./Child";
import { createContext } from "react";

export const AppContext = createContext({});

const App = () => {
  const [checkList, setCheckList] = useState<[]>([]);

  return (
    <AppContext.Provider value={{ checkList, setCheckList }}>
      <Child />
    </AppContext.Provider>
  );
};

export default App;
```

```Child.tsx
import { useContext, useEffect, useState } from "react";
import { AppContext } from "./App";
import { Checkbox, Box } from "@mui/material";

// 初期リスト
const initList = [
  { id: 0, checked: true, name: "list_1" },
  { id: 1, checked: true, name: "list_2" }
] as any[];

const Child = () => {
  const appContext: any = useContext(AppContext);
  const { checkList, setCheckList } = appContext;

  const [list, setList] = useState<any[]>([]);

  // useState と useContext のリスト情報初期セットアップ
  useEffect(() => {
    setCheckList(initList);
    setList(initList);
  }, []);

  const checkConsole = (idx: number) => {
    console.log("useState管理のリスト  ：", list[idx]);
  };

  const handleChange = (item: any) => {
    item.checked = !item.checked;
    const newList = [...list];
    setList(newList);

    checkConsole(item.id);
  };

  return (
    <Box sx={{ textAlign: "center" }}>
      <h1>Create CheckList!!!</h1>
      {list?.map((item: any) => {
        return (
          <Box
            key={item.id}
            sx={{
              fontSize: "36px",
              desplay: "inline-block",
              textAlign: "center"
            }}
          >
            <Checkbox
              size="large"
              checked={item.checked}
              onChange={() => handleChange(item)}
            />
            <label>{item.name}</label>
          </Box>
        );
      })}
    </Box>
  );
};

export default Child;
```

### 検証

画面はこんな感じで出来上がりました。
<img width="400" alt="react_view_1.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537959/2d4f8417-2315-8806-a79e-3d4f4d697995.png">

以下内容のリストを保持し、onChange イベントで`handleChange()`を発火して`checked`を更新することで、チェックボックスの on/off が切り替えられるようになっています。
```tsx
const initList = [
  { id: 1, checked: true, name: "list_1" },
  { id: 2, checked: true, name: "list_2" }
] as any[];
```

実際、意図通りの挙動になっています。（クリックしたら checked が false に更新されチェックが外れる）
<img width="400" alt="react_view_2.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537959/a84e86fd-124b-9ebf-9fb2-fcf258ea1abf.png">

ですが、`handleChange()`の中身がが明らかにおかしいです。
```tsx
  const handleChange = (item: any) => {
    // 何してんのこれ？
    item.checked = !item.checked;

    // 何も起きなくない？
    const newList = [...list];
    setList(newList);

    // なぜ更新されている...？
    checkConsole();
  };
```

`item`
これはuseStateの値を引数として渡したものになりますが、state の直接更新をしているようです。
最終的に`setList()`を実行しているのでViewに反映されていますが、
例えば`setList(newList)`をコメントアウトした場合、以下のように state の値は更新されるのに View は変わらないという動きになってしまいます。
<img width="400" alt="react_test_movie.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537959/45407310-627a-5451-e6f1-7d3b816dfd5c.gif">

実装はおかしいですが、`setList(newList)`をするなら動作的には問題ないように見えます。
ですが、もう一つ問題がありました。

以下も一緒にコンソールに出してみます。
```tsx
  const checkConsole = (idx: number) => {
    console.log("useState管理のリスト  ：", list[idx]);
    console.log("useContext管理のリスト：", checkList[idx]);
    console.log("initList値          ：", initList[idx]);
  };
```
・出力結果
<img width="400" alt="react_view_3.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537959/ad0ccce6-71a7-b17b-4135-4fe83ce71cb1.png">

なぜか`useContext`の値まで更新されている。。（ついでに初期値の配列も）

少し悩み、気づきました。

「あ、initList の**参照渡し**みたいなことが起こっているのか...」

ということでググります。

https://qiita.com/yuta0801/items/f8690a6e129c594de5fb

こちらの内容が一番正確？

javascript は**参照渡し/値渡し**という言葉は当てはまらないようです。
プリミティブ型の場合は**値渡し**、オブジェクトの場合は**参照渡し**という説明もありますが、正確には違うと私も思います。
記事では**値の参照**という言葉が使われています。
オブジェクトの場合はプリミティブ型と違い、この**値の参照**を更新しないケースがあるので、そこに誤解が生まれるのだと思います。

以下のように検証すると、本物の参照渡しとは違うことがわかります。
```ts
const x = { n: 1 };
const y = x;
let z = x;

y.n = 2;
z = { n: 3 };

console.log("x", x); // {n: 2} => yと共通のもの参照しているが、zとは別ものになっている
console.log("y", y); // {n: 2}
console.log("z", z); // {n: 3} => x,yの参照値に影響を与えていない
```
本物の参照渡しは`z = { n: 3 }`としたら x と y の参照値も更新されるはずです。

### 修正

ともあれ、これが原因っぽいので改めてソース見直すと
```tsx
  const handleChange = (item: any) => {
    // この時点で initList/checkList は list と「値の参照」が同じのため更新されたことになっている
    item.checked = !item.checked;
    // newList は list の内容から配列を再生成しているため「値の参照」内容が変更となる
    const newList = [...list];
    setList(newList);
```
という予想になりましたでので、
「**値の参照**が異なる newList に対して更新かければいけるってことでしょ」という考えで修正。
```tsx
  const handleChange = (item: any) => {
    const newList = [...list];
    // 新規生成した配列要素に対して更新をかける
    newList[item.id].checked = !item.checked;
    setList(newList);
```

......治らない(._.)

原因はこれ。
```ts
console.log(`newList===list: ${newList === list}`);
// => false
console.log(`newList[item.id]===list[item.id]: ${newList[item.id] === list[item.id]}`);
// => true
```
-> **配列自体**は同一参照の動きを回避できているが、**配列要素**が同一参照になっている

なるほど...
対象の配列は`[{},{}]`の形式なので`{}`も再生成してあげないと部分的に参照値の共有が発生しうる、ということですね...

ということで、これが正解のひとつかと思います。
```tsx
  const handleChange = (item: any) => {
    // リスト内の要素も新規のオブジェクトで組み直す
    const newList = list.map((item) => ({ ...item }));

    newList[item.id].checked = !item.checked;
    setList(newList);
  };
```

onChange で useState の checked のみが更新されることも確認できました。
<img width="400" alt="react_view_4.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537959/9b944438-0fbb-257e-3cd9-4d1a6e9c05ca.png">

:::note warn
コンソール出力を useState 更新と同関数内で実行していましたが、修正により setState 本来の正しい動きとなったため出力タイミングも修正しています。
（元のソースでは`initList`の直接更新となっていたので即反映されていた）
:::

React（というか javascript）でネストの深いオブジェクトを扱う場合は今回の教訓を生かして十分注意したいと思います。


# おわりに
原因特定には少々時間がかかってしまいました。
javascript の値渡し/参照渡しに関するルールがきちんとわかっている方だったら一瞬で解決する内容だったかと思います。
基礎大事！


# 参考
* https://typescriptbook.jp/reference/functions/pass-by-value
* https://dev.classmethod.jp/articles/arrow-func-return-object/
* https://qiita.com/yuta0801/items/f8690a6e129c594de5fb