ReactのuseStateを使った配列の更新でつまづいた話

## はじめに
Reactについてちゃんと勉強しようと思い、1月中旬ごろからちまちま進め始めた。
Udemyの講座で基礎から確認していると、初っ端のuseStateについてうまく行かないところがあったので、理解を深めるためにまとめてみた。

## つまづいたところ
### 概要
画面上に表示されたリストに対して、要素を追加しようとしていた。

```react
const App = () => {
    const [state, setState] = useState(['apple', 'banana', 'cherry']);
    const updateFruitList = () => {
        const val = document.querySelector("#addFruit").value;
        state.push(val);
        setState(state);
    };
    return (
        <>
            <ul>
                {state.map(fruit => <li key={fruit}>{fruit}</li>)}
            </ul>
            <label htmlFor="addFruit">
                フルーツ：
                <input type="text" id="addFruit"></input>
                <button onClick={updateFruitList}>追加</button>
            </label>
        </>
    );
```
画面は以下の通り表示されたが、追加ボタンを押しても画面上の要素が増えない。

<img width='70%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/fd2e3bd8-f901-e9c6-110c-c66273173279.png'>

 デバッグして中身を確認してみると、state自体は要素が追加されている。
 つまり、中身は更新されているが、再レンダリングが発生していない、ということのように思える。

 <img width='70%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/c3d4f612-42bc-26a4-f559-1bcb597779c5.png'>

### 原因
どうやら配列やオブジェクトをset関数に設定する場合、新しい配列やオブジェクトを作成しなければならないらしい。
```diff_javascript
const updateFruitList = () => {
    const val = document.querySelector("#addFruit").value;
     /* 間違い */
-    state.push(val);
-    setState(state);
     /* 正しい */
+    const newState = [...state];
+    newState.push(val);
+    setState(newState);
     /* これも可（こっちのがシンプル） */
+    setState([...state, val]);
};
```
set関数は設定された値が前と同じものだった場合、再レンダリングをしないらしい。
値が同一かどうかの判定は`Object.is()`によって行われる。
同じ配列を渡すとpushで中身を追加していても参照自体は同じものなので、同一と判断されてしまう。
そのため、新しい配列なり、オブジェクトなりを生成して設定する必要がある。

## つまずきからの発展
### まだ間違いがあった
ただ、上記の書き方もまだ改善の余地がある。

```react
const updateFruitList = () => {
    const val = document.querySelector("#addFruit").value;
    setState([...state, val]);
    setState([...state, val]);
    setState([...state, val]);
};
```
ボタンをクリックされたら、3連続で要素を追加することを考えてみる。
上記コードで実際に試してみると、要素は1度しか追加されない。

これは配列やオブジェクトに限らず、数値や文字列でも同様の事象が発生する。

### set関数の更新タイミング
実は、set関数で設定された値はすぐにstateに反映されるわけではない。

更新は予約状態になっていて、画面がレンダリングされるときに更新される。
つまり、以下のように更新されていないstateに対して次々と値を追加しているので、結果として1つしか要素が追加されない
```react
const updateFruitList = () => {
    const val = document.querySelector("#addFruit").value;
    setState([...state, val]); // ['apple','banana','chery']にたいしてvalを追加する準備
    setState([...state, val]); // stateはまだ変わってないので['apple','banana','chery']にたいしてvalを追加する準備
    setState([...state, val]); // stateはまだ変わってないので['apple','banana','chery']にたいしてvalを追加する準備
```

### 前の値を引き継ぐ
では直前の値に対して処理をするためにはどうすればよいか、というとset関数に対してstateを更新するための関数を引数として渡す。
```react
const updateFruitList = () => {
    const val = document.querySelector("#addFruit").value;
    setState(prev => [...prev, val]);
    setState(prev => [...prev, val]);
    setState(prev => [...prev, val]);
};
```
ここでいうと`prev => [...prev, val]`が更新用の関数にあたる。
stateを更新するための関数は、引数として処理中のstateを受け取り、次のstateを戻り値として返す。
`prev`はただの変数名なのでなんでもいい。
ここでは処理中の値を`prev`として受け取り、戻り値として`prev`の配列をコピーして新しく生成したものに、valを追加したものを渡している。

上記コードで実行すると、1度のクリックで3回要素が追加されていることが確認できる。

## まとめ
配列やオブジェクトをuseStateを使って更新する際は、新しいものを作成してから引き渡すようにする。
そうしないと、変更前後で同一とみなされ、再レンダリングが発生しない。

また、配列やオブジェクトに限らずすべての値について、直前のstateを利用したい場合には、useStateのset関数に値そのものではなく、値を更新するための関数を引き渡す。
関数として渡すと、キューに入れられ、順番に実行されるため、更新されたあとの値に対して処理を行うことができる。

困ったら公式サイトを確認するのが一番つよい、ということも今回再認識できた。

## 参考にしたサイト
* [React - オブジェクトや配列の state を更新する](https://ja.react.dev/reference/react/useState#updating-objects-and-arrays-in-state)
* [React - 直前の state に応じて更新する](https://ja.react.dev/reference/react/useState#updating-objects-and-arrays-in-state)