🤔
React - useEffectのdependencyで無限ループ
Problem
問題は下記のような React Component を作成した時に起こる。
Context を含む Component を作成し、それをuseMyContext()
という名前エクスポートをする。
MyComponent
という Component で state を表示したい。その際、Context からエクスポートされているstate
という Object とupdateState
という Function を呼び出す。
最新のstate
を表示するため、useEffect
の中にupdateState
をstate
の変化のたびに更新するようにする。
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つ)の方法がある。
-
useEffect
の中に Context から渡された Function を使わない。 - 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])
//...
}
- 3 つ目の解決策はまだ現在の React には実装されておらず、まだ RFC の段階である。
useCallback
とuseMemo
を使わなくてもuseEvent
という一つの hook だけで全てが解決される。これはReact Docs Betaで使用法を伝えているのでチェックすることをおすすめする。
Discussion