setState無限ループ

2020/12/19

ここ数日ずーっとハマってたReact無限ループ問題が解決しました。

何が起こっていた?

すごく簡単にするとsetState → レンダリング → setState … のような無限ループが発生していました。

// Test.tsx
const Test: FC = () => {
  const testHooks = useTestHooks();
  const [testValue, setTestValue] = useState(['test']);
  const getResponse = async () => {
    testHooks.then((response) => setTestValue(response));
  };

  useEffect(() => {
    getResponse();
  });

  console.log('Rendering');

  return (
    <>
      <div>{testValue}</div>
    </>
  );
};

export default Test;

// use-test-hooks.ts
const useTestHooks: () => Promise<string[]> = () => {
  const response = new Promise<string[]>((resolve) => {
    resolve(['response1', 'response2']);
  });

  return response;
};

export default useTestHooks;

とあるコンポーネントからuseTestHooksを使って非同期でオブジェクトの配列を取得したかっただけなのですが, 同一のコンポーネント内でsetTestValue()を行っているため,Testコンポーネントをレンダリング → setTestValue()実行 → DOMの差分を検知して,再レンダリングが走る → Testコンポーネントをレンダリング → setTestValue()実行 → 以下無限ループ
「まぁ,そうなるよね。」って感じだと思います。

しかも,普通ならエラーで止まってくれるはず(画像①)なのに,エラーが検知されずにひたすらレンダリングを繰り返されていました(画像②)。
image
image

どう解決したか

ググれば一番はじめにでてくるであろう「useEffect」を使った方法です。

const Test: FC = () => {
  const testHooks = useTestHooks();
  const [testValue, setTestValue] = useState(['test']);
  const getResponse = async () => {
    testHooks.then((response) => setTestValue(response));
  };

  useEffect(() => {
    getResponse();
  }, []);

  console.log('Rendering');

  return (
    <>
      <div>{testValue}</div>
    </>
  );
};

そう,useEffectの第二引数に[]を入れただけ。
こうすることで,レンダリング後に一度だけ第一引数のコールバック関数を実行します。

なぜハマったか

上記の解決策,もちろん一番はじめに試しました。
じゃあ,なぜ解決しなかったのか?
それはESLintに怒られたからです。
image
image
今思えば,この時点で,無視設定いれて試すだけ試せばよかったのにしなかったのが分岐点でした。。。

そしてここからどんどん深みにハマっていきました。
ほぼほぼ同様の実装(下記ソース)で,無限ループが発生していないことから混乱し始めました。
型指定の仕方が悪いのか?非同期なのが悪いのか?setState()を実行するコンポーネントを変えたり,とにかくいろいろ試行錯誤しました。
その結果,さらにわけがわからなくなってしまいました。

// Test.tsx
const Test: FC = () => {
  const testHooks = useTestHooks();
  const [testValue, setTestValue] = useState('test');
  const getResponse = async () => {
    testHooks.then((response) => setTestValue(response));
  };

  useEffect(() => {
    getResponse();
  });

  console.log('Rendering');

  return (
    <>
      <div>{testValue}</div>
    </>
  );
};

// use-test-hooks.ts
const useTestHooks: () => Promise<string> = () => {
  const response = new Promise<string>((resolve) => {
    resolve('response');
  });

  return response;
};

解決への軌跡

混乱しきって,一旦冷静になって整理しようという思考に入り,まずは何が原因なのかを再度明確に切り分けるために,最低限のコードを書いて問題の切り分けを行いました。
その結果,型でも,非同期処理でもなくsetStateが原因ということ。
かつプリミティブ型以外をsetTestValueに入れると無限ループが起きることがわかりました。
しかし,入れたいのはオブジェクトの配列
そしてやりたいのは初回レンダリング後に一回だけsetTestValueするだけ。。。
やっぱりuseEffectに第二引数[]を入れて使うしかない → Lintを無視しよう そして,やっとこさ解決へとたどり着きました。
めちゃくちゃ遠回り。
今思えば,なんでLintを無視するの試すだけ試さなかったのか不思議でならないですね。
だいたいバグを解決するとそんなこと思います。

まとめ

Linterを信用しすぎるな!
(Lintが100%正しいという)思い込みで視野を狭めるな!
これに尽きます。
ちゃんと自分の頭で考えなきゃですね。

それではまた明日。


書いた人: こへ
音楽と漫画と読書とアニメとスノボが好き。多趣味でいろんなことに興味有ります。 誰しもが一度は使った事があるもののIoT開発をしてます。
Twitterフォローお願いします。