0
0

複数の非同期関数で実行されるset更新関数でハマった件

Posted at

はじめに

フロントエンドを 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

  1. そもそも設計が悪いでしょ!というご意見は今回の趣旨ではないのでご容赦ください。本記事の元になった実際の例は、フロントエンド側は一緒のオブジェクトとして扱いたいが、バックエンド側では別々の API かつ、一方の API 実行に著しく時間がかかり、どちらの実行も待っていたら UX が悪いというケースでした。

  2. 実はすでにやってみたが、わけがわからなくて挫折中なのは内緒です。

0
0
3

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
0
0