Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React context not updating

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?

like image 889
kuroneko Avatar asked Mar 15 '20 21:03

kuroneko


People also ask

How do you update a context value React?

To update a React Context from inside a child component, we can wrap the React Context provider around the child components. Then we set the value prop of the context provider to the the state setter function that lets us update the context value. Then we can use the useContext hook to access the context.

Does React Rerender on context change?

Context and React renderingWhen a component renders, React will recursively re-render all its children regardless of props or context.

How does React context work?

What is context in React? React's context allows you to share information to any component, by storing it in a central place and allowing access to any component that requests it (usually you are only able to pass data from parent to child via props).

What is createContext React?

React.createContextWhen React renders a component that subscribes to this Context object it will read the current context value from the closest matching Provider above it in the tree. The defaultValue argument is only used when a component does not have a matching Provider above it in the tree.


3 Answers

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.

like image 56
John Ruddell Avatar answered Oct 17 '22 21:10

John Ruddell


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.

like image 35
strdr4605 Avatar answered Oct 17 '22 21:10

strdr4605


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>
like image 1
Junior Tour Avatar answered Oct 17 '22 20:10

Junior Tour