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")
が実行されてますね。これと同じ原理です。
つまり、これが事件の顛末でしょう。
- state の初期値は
3
-
Button
がレンダリングされてsetFollowingsLimit((prev) => prev + 3)
実行 - state が6に変更
- state の変更を探知し、コンポーネント再レンダリング
-
Button
がレンダリングされてsetFollowingsLimit((prev) => prev + 3)
実行 - state が9に変更
- state の変更を探知し、コンポーネント再レンダリング
-
Button
がレンダリングされてsetFollowingsLimit((prev) => prev + 3)
実行 - state が12に変更
- ....(無限繰り返し)
4. 解決
解決はめっちゃ簡単です。
const loadMoreFollowings = () => setFollowingsLimit((prev) => prev + 3);
const loadMoreFollowers = () => setFollowersLimit((prev) => prev + 3);
このようにsetState
を関数の中に入れて渡したらちゃんとクリックしたときにだけ実行されます。(当たり前ですが)
Javascriptを初めて習うとき、コールバックに渡す関数名に()
つけて渡してコールバックじゃなくなるミスをすることが多いと思いますが、今回もそれでした。
書き終わったらもうすごい当たり前なこと書いてて恥ずかしいです…JSしっかり勉強しろよっていう話ですね。
Discussion