はじめに
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関数に値そのものではなく、値を更新するための関数を引き渡す。
関数として渡すと、キューに入れられ、順番に実行されるため、更新されたあとの値に対して処理を行うことができる。
困ったら公式サイトを確認するのが一番つよい、ということも今回再認識できた。