🤔

React - useEffectのdependencyで無限ループ

2022/10/12に公開

Problem

問題は下記のような React Component を作成した時に起こる。
Context を含む Component を作成し、それをuseMyContext()という名前エクスポートをする。
MyComponentという Component で state を表示したい。その際、Context からエクスポートされているstateという Object とupdateStateという Function を呼び出す。
最新のstateを表示するため、useEffectの中にupdateStatestateの変化のたびに更新するようにする。
ESLint が dependency 配列の中に、updateStateがないと言うのでupdateStateを入れる。
しかし、ここで問題が起きる。
挙動を確認すると、Browser が無限ループに入っている。

// MyComponent.tsx

const { state, updateState } = useMyContext()

useEffect(() => {
  updateState(newState)
}, [updateState, newState, state])

原因はuseEffectが dependency を確認する際、Object.is(A,B)で dependency の変化を確認することにある。
Component が re-render される際、function(object も array も同様)は reference が毎回、変化してしまう。つまり、Object.is(A,B)で dependency を確認する時に毎回、変化があったと認識されてしまい、無限ループに入ってしまう。

Solution

この問題を解決するためには2つ(3つ)の方法がある。

  1. useEffectの中に Context から渡された Function を使わない。
  2. Context の中で定義をする時に Function をuseCallbackで囲み、export する時にuseMemoで囲む。こうすることで、Context の中の Function が同じ reference として保存され、Object.is(A, B)の dependency チェックでも変化がないと判断される。
// AppContext.tsx
export const AppProvider: FC<ProviderProps> = ({ children }) => {
  // useCallbackでFunctionを囲む
  const _addState = useCallback((newState) => {
    dispatch({
      type: "ADD_STATE",
      payload: {
        newState,
      },
    })
  }, [])

  // useMemoで上記のFunctionを囲む
  const addState = useMemo(() => _addState, [_addState])
  //...
}
  1. 3 つ目の解決策はまだ現在の React には実装されておらず、まだ RFC の段階である。useCallbackuseMemoを使わなくてもuseEventという一つの hook だけで全てが解決される。これはReact Docs Betaで使用法を伝えているのでチェックすることをおすすめする。

Let's PROG

Discussion