2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

つまづいたところ

概要

画面上に表示されたリストに対して、要素を追加しようとしていた。

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>
        </>
    );

画面は以下の通り表示されたが、追加ボタンを押しても画面上の要素が増えない。

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

原因

どうやら配列やオブジェクトをset関数に設定する場合、新しい配列やオブジェクトを作成しなければならないらしい。

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で中身を追加していても参照自体は同じものなので、同一と判断されてしまう。
そのため、新しい配列なり、オブジェクトなりを生成して設定する必要がある。

つまずきからの発展

まだ間違いがあった

ただ、上記の書き方もまだ改善の余地がある。

const updateFruitList = () => {
    const val = document.querySelector("#addFruit").value;
    setState([...state, val]);
    setState([...state, val]);
    setState([...state, val]);
};

ボタンをクリックされたら、3連続で要素を追加することを考えてみる。
上記コードで実際に試してみると、要素は1度しか追加されない。

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

set関数の更新タイミング

実は、set関数で設定された値はすぐにstateに反映されるわけではない。

更新は予約状態になっていて、画面がレンダリングされるときに更新される。
つまり、以下のように更新されていないstateに対して次々と値を追加しているので、結果として1つしか要素が追加されない

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を更新するための関数を引数として渡す。

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関数に値そのものではなく、値を更新するための関数を引き渡す。
関数として渡すと、キューに入れられ、順番に実行されるため、更新されたあとの値に対して処理を行うことができる。

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

参考にしたサイト

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?