Home > OS >  How to avoid infite updates in useEffect, when dependency is a function
How to avoid infite updates in useEffect, when dependency is a function

Time:02-16

A hook is checking, if there are impersonation information in storage persisted, when the page renders, and if so, sets those information in the global AppState context.

const impersonateStorageKey = `impersonation_${config.environment}`
// signature const impersonate: (empScope: number) => void
const { currentImpersonation, impersonate } = useAppContext()

useEffect(() => {
    if (!window || !window.localStorage) return;

    const storageString = localStorage.getItem(impersonateStorageKey)
    if (!storageString) return;

    const data = JSON.parse(storageString)
    impersonate(data.currentImpersonation)
}, [impersonateStorageKey])

With a second hook, that is persisting a change in the current impersonated identity to the storage:

useEffect(() => {
    if (!window || !window.localStorage) return;
    localStorage.setItem(impersonateStorageKey, JSON.stringify({ /* ... */}))
}, [currentImpersonation, impersonateStorageKey])

Plus the relevant bits from useAppContext

const useAppContext = () => {
    const { state, dispatch } = useContext(AppContext)
    if (!state) {
        throw new Error("useAppContext must be used within within  AppContextProvider")
    }
  
    const impersonate = (employeeScope: string | number) => dispatch({ type: 'IMPERSONATE', value: employeeScope })
    const currentImpersonation = state.currentImpersonation

    return {
        impersonate,
        currentImpersonation,
    }
}

This is working as it should, but the linter is complaining, that the dependency impersonate is missing from the first useEffect hook

When I add impersonate to the dependency array, this will cause a constant update loop and make the application unresponsive.

I know what is causing this behaviour, but I am failing to see a solution (except ignoring the rule) on how to break the loop and make the linter happy.

What approaches can I take here?

CodePudding user response:

You can memoize the function when you create it with useCallback:

const useAppContext = () => {
  const { state, dispatch } = useContext(AppContext)

  const impersonate = useCallback(
    (employeeScope: string | number) => dispatch({
      type: 'IMPERSONATE',
      value: employeeScope
    }), [dispatch])

  if (!state) {
    throw new Error("useAppContext must be used within within  AppContextProvider")
  }

  const currentImpersonation = state.currentImpersonation

  return {
    impersonate,
    currentImpersonation,
  }
}

If you can't, a workaround would be to put the function in a ref. The ref is an immutable reference to an object, with the current property which is mutable. Since the ref itself is immutable, you can use it as a dependency without causing useEffect to activate (the linter knows it, and you don't even need to state it as a dependency). You can mutate current as much as you like.

const impersonateRef = useRef(impersonate)

useEffect(() => {
  impersonateRef.current = impersonate
}, [impersonate])

useEffect(() => {
  if (!window || !window.localStorage) return

  const storageString = localStorage.getItem(impersonateStorageKey)
  if (!storageString) return

  const data = JSON.parse(storageString)
  impersonateRef.current(data.currentImpersonation)
}, [impersonateStorageKey])
  • Related