Magnus
Magnus

Reputation: 7811

Gatsby: Context update causes infinite render loop

I am trying to update context once a Gatsby page loads.

The way I did it, the context is provided to all pages, and once the page loads the context is updated (done with useEffect to ensure it only happens when the component mounts).

Unfortunately, this causes an infinite render loop (perhaps not in Firefox, but at least in Chrome).

Why does this happen? I mean, the context update means all the components below the provider are re-rendered, but the useEffect should only run once, and thats when the component mounts.

Here is the code: https://codesandbox.io/s/6l3337447n

The infinite loop happens when you go to page two (link at bottom of page one).

What is the solution here, if I want to update the context whenever a page loads?

Upvotes: 0

Views: 987

Answers (2)

TemporaryFix
TemporaryFix

Reputation: 2186

The correct answer for this issue is not to pass an empty dependency array to useEffect but to wrap your context's mergeData in a useCallback hook. I'm unable to edit your code but you may also need to add dependencies to your useCallback like in my example below

import React, { useState, useCallback } from "react"

const defaultContextValue = {
  data: {
    // set initial data shape here
    menuOpen: false,
  },
  mergeData: () => {},
}

const Context = React.createContext(defaultContextValue)
const { Provider } = Context

function ContextProviderComponent({ children }) {
  const [data, setData] = useState({
    ...defaultContextValue,
    mergeData, // shorthand method name
  })

  const mergeData = useCallback((newData) {
    setData(oldData => ({
      ...oldData,
      data: {
        ...oldData.data,
        ...newData,
      },
    }))
  }, [setData])

  return <Provider value={data}>{children}</Provider>
}

export { Context as default, ContextProviderComponent }

The selected answer is incorrect because the react docs explicitly say not to omit dependencies that are used within the effect which the current selected answer is suggesting.

If you use es-lint with the eslint-plugin-react-hooks it will tell you this is incorrect.

Note

If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders. Learn more about how to deal with functions and what to do when the array changes too often.

https://reactjs.org/docs/hooks-effect.html

Is it safe to omit functions from the list of dependencies? Generally speaking, no. It’s difficult to remember which props or state are used by functions outside of the effect. This is why usually you’ll want to declare functions needed by an effect inside of it. Then it’s easy to see what values from the component scope that effect depends on:

https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies

Upvotes: 1

Derek Nguyen
Derek Nguyen

Reputation: 11577

By default, useEffect runs every render. In your example, useEffect updates the context every render, thus trigger an infinite loop.

There's this bit in the React doc:

If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.

So applies to your example:

  useEffect(() => {
    console.log("CONTEXT DATA WHEN PAGE 2 LOADS:", data)
    mergeData({
      location,
    })
-  }, [location, mergeData, data])
+  }, [])

This way, useEffect only runs on first mount. I think you can also leave location in there, it will also prevent the infinite loop since useEffect doesn't depend on the value from context.

Upvotes: 0

Related Questions