zukucode
主にWEB関連の情報を技術メモとして発信しています。

React useEffectで無限ループが発生するときに確認すること

ReactuseEffectを利用したときに無限ループが発生してしまうことがあります。

特に注意したいのが、ESLintreact-hooks/exhaustive-depsで表示された警告をUpdate the dependencies array to be :[...のコマンドで修正するケースです。

以下は初期表示時にユーザーの一覧をAPIなどで取得して、stateに設定しています。

コンポーネントの処理が実行された後、useEffectの第2引数が変更されている場合のみ、第1引数のファンクションが実行されますが、ここで指定しているsetUsersは変更されることがないため、結果的に初期表示時のみ処理が行われます。(無限ループは発生しない)

import { VFC } from 'react';
import { getUsers } from './api';

const Menu: VFC = () => {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    const list = getUsers();
    setUsers(list);
  }, [setUsers]);

  return <UserList users={users}>;
};

export default Menu;

ここで、getUsersの処理を別のファンクションに分割します。

import { VFC } from 'react';
import { getUsers } from './api';

const Menu: VFC = () => {
  const [users, setUsers] = useState<User[]>([]);

  const load = () => {
    return getUsers();
  };

  useEffect(() => {
    const list = load();
    setUsers(list);
  }, [setUsers]); // 警告が表示される

  return <UserList users={users}>;
};

export default Menu;

ESLintreact-hooks/exhaustive-depsを設定している場合、以下の警告が表示されます。

React Hook useEffect has a missing dependency: 'load'. Either include it or remove the dependency array.

このuseEffectの処理はloadに依存しているので、loadを第2引数に追加しなさい。という警告なので、指摘通りに修正すると警告は消えます。

useEffect(() => {
  const list = load();
  setUsers(list);
}, [setUsers]); // 警告が表示される
}, [setUsers, load]); // 警告は消える

この状態で実行すると無限ループが発生します。

原因

loadを第2引数に追加したことが原因です。

loadはファンクションなので、値が変更されるというイメージをしにくいのですが、コンポーネントの処理が実行されるたびにloadファンクションは新しく作成されます。

コンポーネントのファンクションは初回実行時とsetUsersにより再レンダリングされる時でそれぞれ実行されますが、初回実行時のloadファンクションと、setUsersにより再レンダリング時のloadファンクションは別物として判定されます。

そのため、setUsersによる再レンダリング後に、もう一度ユーザーを取得する処理(useEffectの第1引数のファンクション)が実行されてしまいます。

以下のように無限ループが発生します。

setUsersによる再レンダリング

loadが変更されたためuseEffectの第1引数のファンクションを実行

setUsersによる再レンダリング

無限ループ発生!

対策

以下のようにloaduseCallbackで囲います。

import { VFC, useCallback } from 'react';
import { getUsers } from './api';

const Menu: VFC = () => {
  const [users, setUsers] = useState<User[]>([]);

  const load = useCallback(() => {
    return getUsers();
  }, []);

  useEffect(() => {
    const list = load();
    setUsers(list);
  }, [setUsers, load]);

  return <UserList users={users}>;
};

export default Menu;

useCallbackで囲われたファンクションはコンポーネントファンクションの再実行時に、再生成されなくなります。

useEffectと同じように、第2引数に依存関係の配列を設定し、依存関係のいづれかの値が変更されている場合に再生成がされます。

例えばuseCallbackの処理の中でStateを参照している場合は、依存関係に参照しているStateを追加し、Stateの値が変更された場合はuseCallbackの処理を再生成する必要があります。(再生成をしないと古いStateの値を参照してしまうので)

オブジェクトの場合

ファンクションではなくオブジェクトの場合も同様に無限ループが発生します。

オブジェクトの値に変更がなくても、オブジェクトは毎回再生成されるため、paramは毎回変更されたと判定されます。

import { VFC } from 'react';
import { getUsers } from './api';

const Menu: VFC = () => {
  const [users, setUsers] = useState<User[]>([]);

  const param = { id: 1 };

  useEffect(() => {
    const list = getUsers(param);
    setUsers(list);
  }, [setUsers, param]);

  return <UserList users={users}>;
};

export default Menu;

ファンクションの場合はuseCallbackでしたが、オブジェクトの場合はuseMemoを使用します。

import { VFC, useMemo } from 'react';
import { getUsers } from './api';

const Menu: VFC = () => {
  const [users, setUsers] = useState<User[]>([]);

  const param = useMemo(() => { id: 1 }, []); // 依存関係がないので再生成されることはない

  useEffect(() => {
    const list = getUsers(param);
    setUsers(list);
  }, [setUsers, param]);

  return <UserList users={users}>;
};

export default Menu;

第2引数の依存関係の考え方はuseCallbackと同じです。

まとめ

useMemouseCallbackはパフォーマンス対策として使用する記事が多いですが、useMemouseCallbackを使用することにより、依存関係をはっきりさせて、useEffectの予期せぬ処理を防ぐことができます。

以下の例ではuseEffectの依存関係を辿っていくと、paramに依存していることがわかり、paramは再生成されることはないので、useEffectは初回のみ実行されるということがわかります。

useMemoとuseEffectを組み合わせる例
import { VFC, useCallback, useMemo } from 'react';
import { getUsers } from './api';

const Menu: VFC = () => {
  const [users, setUsers] = useState<User[]>([]);

  const param = useMemo(() => { id: 1 }, []); // 依存関係がないので再生成されることはない

  const load = useCallback(() => {
    return getUsers(param);
  }, [param]); // paramに依存(paramが変更されたら再生成)

  useEffect(() => {
    const list = load();
    setUsers(list);
  }, [setUsers, load]); // loadに依存(loadが変更されたら実行)

  return <UserList users={users}>;
};

export default Menu;

上記例のように、コンポーネント内のStateなどを使用していない場合は、コンポーネント内に定義するのではなく、コンポーネントの外に定義したほうが、オブジェクトが再生成されることもなく、依存関係も考えなくていよいので考え方は簡単になります。(ファンクションも同様です)

import { VFC } from 'react';
import { getUsers } from './api';

const param = { id: 1 };

const Menu: VFC = () => {
  const [users, setUsers] = useState<User[]>([]);

  const param = { id: 1 };

  useEffect(() => {
    const list = getUsers(param);
    setUsers(list);
  }, [setUsers]); // paramは不要

  return <UserList users={users}>;
};

export default Menu;

関連記事