🤔
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