Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

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

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

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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?

>Solution :

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])
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading