Reactで一つのStateを複数のコルーチンから更新する
reactちょっと時間のかかる複数の処理をasync関数で並列に実行する。 これらのうち、終わったタスクの数をstateに持っておいて、進行状態を表示したい。 というときに、自然に書くのは
import { useState } from "react";
const task = (n: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, n * 1000));
const Example = (): JSX.Element => {
const [num, setNum] = useState(0);
const runTask = async (n: number): Promise<void> => {
await task(n);
console.log("done task", n, "num =", num);
setNum(num + 1);
};
return (
<div>
<button
onClick={async () => {
const tasks = [] as Promise<void>[];
for (let i = 1; i <= 3; i++) {
tasks.push(runTask(i));
}
await Promise.all(tasks);
console.log("done all task");
}}
>
start
</button>
<p>num={num}</p>
</div>
);
};
export default Example;
こんな感じ。なんだけど、これだと、画面にはnum=1
が表示される。
なんでかっていうと、
- ボタンが押される
runTask
プロミスが3つ作られる- このとき、この3つのプロミスの後続になるクロージャから見えている
num
の値は全て同一の0 - なので、
await task(n)
のあとのsetNum(num + 1)
は全てsetNum(0 + 1)
という同じ呼び出しになってしまう
解決策は、
setNum((n) => n + 1)
と、stateの更新関数に関数を渡すこと。