React useEffectで無限ループが発生するときに確認すること
React
のuseEffect
を利用したときに無限ループが発生してしまうことがあります。
特に注意したいのが、ESLint
のreact-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;
ESLint
のreact-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
による再レンダリング
↓
無限ループ発生!
対策
以下のようにload
をuseCallback
で囲います。
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
と同じです。
まとめ
useMemo
やuseCallback
はパフォーマンス対策として使用する記事が多いですが、useMemo
やuseCallback
を使用することにより、依存関係をはっきりさせて、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;