川獺の外部記憶

なんでも残しておく闇鍋みたいな備忘録

reactのuseStateのsetterは遅延要素?

もしかしたら超当たり前のことかもしれませんが、少しハマったので残します。

下記リポジトリのハイライト行です。

コードは修正例の方でも理想的なコーディングをしようとしたものではありません。あくまで事象の再現のためなので、色々とマネはしないでください。

gitlab.com

  useEffect(
    () => {
      // 1. ここでリスト(data)を初期化して
      console.log("TEST");
      setData(initialData);
      axios.get(API_BASEURL + "comments/" + `?userId=${selectedState.selectedUserId}`)
        .then((response) => {
          // 本来はここでループを回していちいちsetDataする必要はありません。再現のためです。
          for (let i = 0; i < response.data.length; i++) {
            const currentData = response.data[i];
            console.log([...data, { id: currentData.id, content: currentData.content }]);
            setData([...data, { id: currentData.id, content: currentData.content }]); // ←ココが問題
          }
        })
    }, [selectedState.selectedUserId]
  );

ここではuseEffectを使ってselectedState.selectedUserIdを監視し、selectedState.selectedUserIdに変化があった時に

  1. setDataを使ってdataを初期化(サイズ0の配列にする)
  2. axiosのgetを使ってAPIを叩き、得られた要素全てについてsetDataを使ってdataを追加

を行っています。

この結果、下図のようになることを期待していましたが、

実際は下図のようになってしまいます。

これの修正のためにはuseEffect()を以下のようにすればいけました。

  useEffect(
    () => {
      // 1. ここでリスト(data)を初期化して
      console.log("TEST");
      setData(initialData);
      var tmp: UserComment[] = []; // ← 一時領域を確保しておく
      axios.get(API_BASEURL + "comments/" + `?userId=${selectedState.selectedUserId}`)
        .then((response) => {
          // 本来はここでループを回していちいちsetDataする必要はありません。再現のためです。
          for (let i = 0; i < response.data.length; i++) {
            const currentData = response.data[i];
            console.log([...data, { id: currentData.id, content: currentData.content }]);
            tmp = [...tmp, { id: currentData.id, content: currentData.content }]; // ← 一時領域にためていって
            setData(tmp); // ← ここでまとめてsetDataする(ループ内で毎回やってるけど)
          }
        })
    }, [selectedState.selectedUserId]
  );

考察

しっかりとした仕様レベルの理解はしていません。

今回のsetData()はuseStateで取得したsetterです。 このsetterでdataに格納した情報はワンテンポ*1遅れてdataに反映されると考えられます。

なので、ループ内の次の周で参照したdataの中身は空っぽのまま、という状態が発生しているものと考えられます。

厳密な遅延?の仕様については追って理解して書こうと思いますが、取り急ぎハマった事象をメモしました。

おまけ

ぶっちゃけ今回は以下の通り書くのが一番だと思います。本番で使うならあとはエラー処理も必要。

  useEffect(
    () => {
      axios.get(API_BASEURL + "comments/" + `?userId=${selectedState.selectedUserId}`)
        .then((response) => {
          setData(response.data);
        })
    }, [selectedState.selectedUserId]
  );

*1:どれぐらい?おそらくはuseEffect()が終わったタイミングで反映されると思われる。reactのコンポーネントライフサイクルと関係している気がしますがどうなんだろうか。