はじめに
フロントエンドを React で書いていて非同期関数内で実行される更新関数(set 関数)でハマったのでメモしておきます。
実際に起こったこと
以下のように外部 API からオブジェクトのデータを別々に取得して、それぞれのデータでオブジェクトを更新する処理を書いたとします1。
API を叩いた結果を用いたそれぞれの更新関数 setHuman の引数は関数ではなく、現在の値を利用した値で更新しています。
import React, { useState } from "react";
const sleep = async (sec) => new Promise((resolve) => setTimeout(resolve, sec));
const mockGetNameApi = async () => {
await sleep(100);
return "hoge";
};
const mockGetAgeApi = async () => {
await sleep(500);
return 28;
};
function App() {
const [human, setHuman] = useState({ name: "fuga", age: 0 });
const handleClick = async () => {
mockGetNameApi().then((v) => {
setHuman({ ...human, name: v });
});
mockGetAgeApi().then((v) => {
setHuman({ ...human, age: v });
});
};
return (
<div>
<h1>
Name: {human.name}, Age: {human.age}
</h1>
<button onClick={handleClick}>Call API</button>
</div>
);
}
export default App;
期待する挙動は、ボタン押下して二つの更新関数を実行する handleClick が実行されると Name: hoge, Age: 28 と表示されることですが、実際には Name: fuga, Age: 28 と表示されてしまいます。
しかも何やら Name:fuga, Age:0 と表示された後に Name: hoge, Age: 28 と表示されているようです。
対策
まずは解決策の結論から述べます。
setHuman の引数を前回の値を利用する関数として定義することで解決できます。
import React, { useState } from "react";
...
const handleClick = async () => {
mockGetNameApi().then((v) => {
setHuman((prev) => ({ ...prev, name: v });
});
mockGetAgeApi().then((v) => {
setHuman((prev) => ({ ...prev, age: v });
});
};
なぜか
調べてみると、こちらの記事がとても参考になりました。
こちらの記事にもあるように、コンポーネント内の状態をスナップショットとして保有しているみたいで、それが非同期関数であってもその非同期関数を実行した時点でのスナップショットを利用するようです。
つまり handleClick を呼び出した時点で human は{ name: "fuga", age: 0 } となっており、mockGetAgeApi, mockGetNameApi 実行後の then で利用するそれぞれの human は setHuman の実行に関わらずどちらも{name:"fuga",age:0}となっているようです。
最終的に mockGetAgeApi 側の setHuman が反映されて Name: fuga, Age: 28 と表示されてしまうと解釈しました。
const handleClick = async () => {
mockGetNameApi().then((v) => {
// human は { name: "fuga", age: 0 }
setHuman({ ...human, name: v });
});
mockGetAgeApi().then((v) => {
// 上のsetHumanが実行済みでもhumanはhandleClick実行時の{ name: "fuga", age: 0 } のまま
setHuman({ ...human, age: v });
});
};
また、非同期関数内に関わらず関数内で複数回 set 更新関数を呼び出すと今回のようになるみたいです。
終わりに
今までなんとなく使っていた useState について少し理解が深まった気がします。
今後は set 更新関数の引数に迷うことなく利用できそうです。
また、今度実際に React のソースコードを読んでみて自分自身で理解を深めていきたいなと思います。2