React 新人あるあるのコンポーネント無限レンダリングにはまりました


React 勉強でツイッターみたいな SNS を一人で作ってましたが、プロフィールページの Followings&Followers 情報を読み込むときずっとこのエラーが出てました。

Unhandled Runtime Error
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

レンダリング数が多すぎると怒ってますね。

「はあ!?フォロワー1人しかいないのに何言ってるんだこいつ」とか思いましたが、やはりコードは嘘つきません。悪いのは私でした。

1. 最初のコード

大体こんな感じのコンポーネントを書いてました。swr でデータフェッチして、Followings&Followers の数は最初に3件表示、 More ボタンクリックしたら 3件ずつ表示件数を増やすロジックです。

const fetcher = (url) => axios.get(url, { withCredentials: true }).then((result) => result.data);

const Profile = () => {
  const { me } = useSelector((state) => state.user);
  const [followersLimit, setFollowersLimit] = useState(3);
  const [followingsLimit, setFollowingsLimit] = useState(3);

  const { data: followersData, error: followerError } = useSWR(`${backUrl}/user/followers?limit=${followersLimit}`, fetcher);
  const { data: followingsData, error: followingError } = useSWR(`${backUrl}/user/followings?limit=${followingsLimit}`, fetcher);

  useEffect(() => {
    if (!(me && me.id)) Router.push('/');
  }, [me && me.id]);

  const loadMoreFollowings = setFollowingsLimit((prev) => prev + 3);
  const loadMoreFollowers = setFollowersLimit((prev) => prev + 3);

  if (!me) return 'Loading...';

  if (followerError || followingError) {
    console.error(followerError || followingError);
    return 'ロード中にエラーが発生しました。';
  }

  return (
    <>
      <Head>
        <title>My Profile | NodeBird</title>
      </Head>
      <AppLayout>
        <NicknameEditForm />
        <FollowList header="Following List" data={followingsData} onClickMore={loadMoreFollowings} loading={!followingsData && !followingError} />
        <FollowList header="Follower List" data={followersData} onClickMore={loadMoreFollowers} loading={!followersData && !followerError} />
      </AppLayout>
    </>
  );
};

React を結構使ってる方々はこれだけ見て誰が無限ループの犯人かすぐわかると思いますが、私みたいなひよこはどこが悪くて無限レンダリングしてるのかもわからなくて2日も悩んでました😇

結論から言いますと、犯人はこいつでした

const loadMoreFollowings = setFollowingsLimit((prev) => prev + 3);
const loadMoreFollowers = setFollowersLimit((prev) => prev + 3);

More ボタンのonClickコールバックで渡したsetState関数です。

2. React のレンダリング

以前 React クラスコンポーネントの説明でthis.state = 'hoge'のように state を直接操作するのではなくてsetStateメソッドを使うべきという話を聞いたことがあります。Reactは state の変更を探知して再レンダリングを行いますが、直接 state を操作する場合はその変化が探知できなくて再レンダリングができないことが理由でした。

Hooks の登場以降はuseStateを使って関数コンポーネントでも state 管理ができるようになりました。

const [state, setState] = useState(initialState);

今回はこのsetStateの性質をすっかり忘れていたことが無限ループの始まりでしたね。setStateについては公式サイトで簡単に説明してくれてます。

setState 関数は state を更新するために使用します。新しい state の値を受け取り、コンポーネントの再レンダーをスケジューリングします。

コンポーネントを…再レンダー…

3. 無限ループに至る経緯

それではどの経緯で無限ループにハマったかを解説します。(loadMoreFollowings も loadMoreFollowers もロジックが全く同じなので loadMoreFollowings だけで説明します)

const loadMoreFollowings = setFollowingsLimit((prev) => prev + 3);

この関数はonClickのコールバックで渡す関数なので、結果的には以下のようになると思います。

<Button onClick={setFollowingsLimit((prev) => prev + 3)} loading={loading}>More</Button>

そして、これはクリックをしたときにsetFollowingsLimitを実行するのではなくてButtonコンポーネントがレンダリングされたときに実行することになってしまいます。

簡単な例でonClick={console.log("rendered")}が入ってるボタンを作りました。コンソール窓を開くと、ボタンクリックしてないのにconsole.log("rendered")が実行されてますね。これと同じ原理です。

つまり、これが事件の顛末でしょう。

  1. state の初期値は3
  2. ButtonがレンダリングされてsetFollowingsLimit((prev) => prev + 3)実行
  3. state が6に変更
  4. state の変更を探知し、コンポーネント再レンダリング
  5. ButtonがレンダリングされてsetFollowingsLimit((prev) => prev + 3)実行
  6. state が9に変更
  7. state の変更を探知し、コンポーネント再レンダリング
  8. ButtonがレンダリングされてsetFollowingsLimit((prev) => prev + 3)実行
  9. state が12に変更
  10. ....(無限繰り返し)

4. 解決

解決はめっちゃ簡単です。

const loadMoreFollowings = () => setFollowingsLimit((prev) => prev + 3);
const loadMoreFollowers = () => setFollowersLimit((prev) => prev + 3);

このようにsetState関数の中に入れて渡したらちゃんとクリックしたときにだけ実行されます。(当たり前ですが)

Javascriptを初めて習うとき、コールバックに渡す関数名に()つけて渡してコールバックじゃなくなるミスをすることが多いと思いますが、今回もそれでした。


書き終わったらもうすごい当たり前なこと書いてて恥ずかしいです…JSしっかり勉強しろよっていう話ですね。