React context not updating
Asked Answered
D

3

24

I have set a basic sample project that use Context to store the page title, but when I set it the component is not rerendered.

Principal files:

Context.js

import React from 'react'

const Context = React.createContext({})

export default Context

AppWrapper.js

import React from 'react'
import App from './App'
import Context from './Context'

function AppWrapper () {
  return (
    <Context.Provider value={{page: {}}}>
      <App />
    </Context.Provider>
  )
}

export default AppWrapper

App.js

import React, { useContext } from 'react';
import Context from './Context';
import Home from './Home';

function App() {
  const { page } = useContext(Context)
  return (
    <>
      <h1>Title: {page.title}</h1>
      <Home />
    </>
  );
}

export default App;

Home.js

import React, { useContext } from 'react'
import Context from './Context'

function Home () {
  const { page } = useContext(Context)
  page.title = 'Home'

  return (
    <p>Hello, World!</p>
  )
}

export default Home

full code

What am I doing wrong?

Dunn answered 15/3, 2020 at 21:16 Comment(5)
pretty sure you need to put the value inside state in the App component. and when you want to change it you need to setState, you could pass a setter down if you want to let other components set the value <Context.Provider value={{setContext: mySetter, {...state}}>Quianaquibble
You can't change the page.title by doing page.title = 'Home'Slavism
@JohnRuddell in App or AppWrapper? I did something like that but then in the console I had react complaining that a child was modifying the state of a parent component iircDunn
@Dunn the place where you render the context provider. heres an example. codesandbox.io/s/dreamy-darkness-vi76j if you'd like me to write it up as an answer I will :)Quianaquibble
@JohnRuddell yes pleaseDunn
Q
23

Think about React context just like you would a component, if you want to update a value and show it then you need to use state. In this case your AppWrapper where you render the context provider is where you need to track state.

import React, {useContext, useState, useCallback, useEffect} from 'react'

const PageContext = React.createContext({})

function Home() {
  const {setPageContext, page} = useContext(PageContext)
  // essentially a componentDidMount
  useEffect(() => {
    if (page.title !== 'Home')
      setPageContext({title: 'Home'})
  }, [setPageContext])
  return <p>Hello, World!</p>
}

function App() {
  const {page} = useContext(PageContext)
  return (
    <>
      <h1>Title: {page.title}</h1>
      <Home />
    </>
  )
}

function AppWrapper() {
  const [state, setState] = useState({page: {}})
  const setPageContext = useCallback(
    newState => {
      setState({page: {...state.page, ...newState}})
    },
    [state, setState],
  )
  const getContextValue = useCallback(
    () => ({setPageContext, ...state}),
    [state, updateState],
  )
  return (
    <PageContext.Provider value={getContextValue()}>
      <App />
    </PageContext.Provider>
  )
}

Edit - Updated working solution from linked repository

I renamed a few things to be a bit more specific, I wouldn't recommend passing setState through the context as that can be confusing and conflicting with a local state in a component. Also i'm omitting chunks of code that aren't necessary to the answer, just the parts I changed

src/AppContext.js

export const updatePageContext = (values = {}) => ({ page: values })
export const updateProductsContext = (values = {}) => ({ products: values })

export const Pages = {
  help: 'Help',
  home: 'Home',
  productsList: 'Products list',
  shoppingCart: 'Cart',
}

const AppContext = React.createContext({})

export default AppContext

src/AppWrapper.js

const getDefaultState = () => {
  // TODO rehydrate from persistent storage (localStorage.getItem(myLastSavedStateKey)) ?
  return {
    page: { title: 'Home' },
    products: {},
  }
}

function AppWrapper() {
  const [state, setState] = useState(getDefaultState())

  // here we only re-create setContext when its dependencies change ([state, setState])
  const setContext = useCallback(
    updates => {
      setState({ ...state, ...updates })
    },
    [state, setState],
  )

  // here context value is just returning an object, but only re-creating the object when its dependencies change ([state, setContext])
  const getContextValue = useCallback(
    () => ({
      ...state,
      setContext,
    }),
    [state, setContext],
  )
  return (
    <Context.Provider value={getContextValue()}>
      ...

src/App.js

...
import AppContext, { updateProductsContext } from './AppContext'

function App() {
  const [openDrawer, setOpenDrawer] = useState(false)
  const classes = useStyles()
  const {
    page: { title },
    setContext,
  } = useContext(Context)

  useEffect(() => {
    fetch(...)
      .then(...)
      .then(items => {
        setContext(updateProductsContext({ items }))
      })
  }, [])

src/components/DocumentMeta.js

this is a new component that you can use to update your page names in a declarative style reducing the code complexity/redundancy in each view

import React, { useContext, useEffect } from 'react'
import Context, { updatePageContext } from '../Context'

export default function DocumentMeta({ title }) {
  const { page, setContext } = useContext(Context)

  useEffect(() => {
    if (page.title !== title) {
      // TODO use this todo as a marker to also update the actual document title so the browser tab name changes to reflect the current view
      setContext(updatePageContext({ title }))
    }
  }, [title, page, setContext])
  return null
}

aka usage would be something like <DocumentMeta title="Whatever Title I Want Here" />


src/pages/Home.js

each view now just needs to import DocumentMeta and the Pages "enum" to update the title, instead of pulling the context in and manually doing it each time.

import { Pages } from '../Context'
import DocumentMeta from '../components/DocumentMeta'

function Home() {
  return (
    <>
      <DocumentMeta title={Pages.home} />
      <h1>WIP</h1>
    </>
  )
}

Note: The other pages need to replicate what the home page is doing

Remember this isn't how I would do this in a production environment, I'd write up a more generic helper to write data to your cache that can do more things in terms of performance, deep merging.. etc. But this should be a good starting point.

Quianaquibble answered 16/3, 2020 at 6:24 Comment(16)
Would it be similar if I have more values in my context? like page and product?Dunn
I mean, my basic example had only one value in its context but in my real project I have various and I am unsure how to use useCallback with more than one valueDunn
Btw the solution from @Slavism looks easier but I wonder if yours is more correctDunn
Could you please update your example for a context with more than one field, please? Like say page.title and page.subtitle. Thank youDunn
@Dunn Ah so its more than just page information, ok well you can adjust this to work how you need to, I was trying to make it easier in terms of updating the page data, so that you dont have to on every update include the page: { nesting. Use your best judgement here for what works in your use case. These answers shouldn't be just copied directly into your code, but rather a guideline of how to solve your specific issue.Quianaquibble
I'm using useCallback to not re-create the value object every render cycle, that would cause a re-render to all children, which is really inefficient considering this is the parent component to your whole application. you can update the setter to not nest everything inside page. The concern I would have if I were in your shoes is someone tries to update the context and accidentally removes a vital piece in your data (like page for instance). if they were to setContext({product: <some value here>}) all of the sudden page would be missing. I'd try to mitigate that if you can :)Quianaquibble
If you want to post more details about the structure of your context i'd be happy to help you define a better way to update parts of it.Quianaquibble
Thank you. I really don't understand how useCallback works :/ if you have time you can have a look at my not working code and a try of implementing the other solution that doesn't work either (the title show very shortly and then disappears) Thanks you a lot to try to help meDunn
Ok cool, so are you just wanting to update the page title when a change happens? how do you want me to give you the updated code?Quianaquibble
Currently, I want two things in my context. The page title, and a list of product fetched from a remote JSON the context should look like { page: { title: 'current page title'}, products { items: _the JSON content_ } } what I don't understand is if i need more than one useCallback or how to set thatDunn
Yea understand that, I have fixed the title and products issues you're having. trying to figure out how i should give it back to uQuianaquibble
what do you mean by "how do you want me to give you the updated code?"? Just edit the code of this answer, or if you want fork my repoDunn
Well, its a number of changes lol. But sure i'll just post it here.Quianaquibble
First of all thank you very much. I edited the code following your instructions but for some reason the fetch of the json doesn't work. you can have a look of the code at git.disroot.org/soratobuneko/myMicroShop/src/branch/contextDunn
The React developer tools shows me that the Context.Provider value are correctly populated, but for some reason when I try to access the { products } from my ProductsList component, its an empty objectDunn
my fault. I had wrote "product" instead of "products" in AppContext. solvedDunn
S
6

Here is a working version of what you need.

import React, { useState, useContext, useEffect } from "react";
import "./styles.css";

const Context = React.createContext({});

export default function AppWrapper() {
  // creating a local state
  const [state, setState] = useState({ page: {} });

  return (
    <Context.Provider value={{ state, setState }}> {/* passing state to in provider */}
      <App />
    </Context.Provider>
  );
}

function App() {
  // getting the state from Context
  const { state } = useContext(Context);
  return (
    <>
      <h1>Title: {state.page.title}</h1>
      <Home />
    </>
  );
}

function Home() {
  // getting setter function from Context
  const { setState } = useContext(Context);
  useEffect(() => {
    setState({ page: { title: "Home" } });
  }, [setState]);

  return <p>Hello, World!</p>;
}

Edit long-lake-ct7yu

Read more on Hooks API Reference.

Slavism answered 15/3, 2020 at 21:31 Comment(1)
This is not very useful - no app runs in a single filePohl
B
4

You may put useContext(yourContext) at wrong place.

The right position is inner the <Context.Provider>:

// Right: context value will update
<Context.Provider>
  <yourComponentNeedContext />
</Context.Provider>

// Bad: context value will NOT update
<yourComponentNeedContext />

<Context.Provider>
</Context.Provider>
Bounder answered 15/12, 2021 at 11:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.