简体   繁体   中英

Why isn't `useContext` re-rendering my component?

As per the docs :

When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate , a rerender will still happen starting at the component itself using useContext. ... A component calling useContext will always re-render when the context value changes.

In my Gatsby JS project I define my Context as such:

Context.js

import React from "react"

const defaultContextValue = {
  data: {
    filterBy: 'year',
    isOptionClicked: false,
    filterValue: ''
  },
  set: () => {},
}

const Context = React.createContext(defaultContextValue)

class ContextProviderComponent extends React.Component {
  constructor() {
    super()

    this.setData = this.setData.bind(this)
    this.state = {
      ...defaultContextValue,
      set: this.setData,
    }
  }

  setData(newData) {
    this.setState(state => ({
      data: {
        ...state.data,
        ...newData,
      },
    }))
  }

  render() {
    return <Context.Provider value={this.state}>{this.props.children}</Context.Provider>
  }
}

export { Context as default, ContextProviderComponent }

In a layout.js file that wraps around several components I place the context provider:

Layout.js :

import React from 'react'
import { ContextProviderComponent } from '../../context'

const Layout = ({children}) => {

    return(
        <React.Fragment>
            <ContextProviderComponent>
                {children}
            </ContextProviderComponent>
        </React.Fragment>
    )
}

And in the component that I wish to consume the context in:

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

const Visuals = () => {

    const filterByYear = 'year'
    const filterByTheme = 'theme'

    const value = useContext(Context)
    const { filterBy, isOptionClicked, filterValue } = value.data

    const data = <<returns some data from backend>>

    const works = filterBy === filterByYear ? 
        data.nodes.filter(node => node.year === filterValue) 
        : 
        data.nodes.filter(node => node.category === filterValue)

   return (
        <Layout noFooter="true">
            <Context.Consumer>
                {({ data, set }) => (
                    <div onClick={() => set( { filterBy: 'theme' })}>
                       { data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1> }
                    </div>
                )
            </Context.Consumer>
        </Layout>
    )

Context.Consumer works properly in that it successfully updates and reflects changes to the context. However as seen in the code, I would like to have access to updated context values in other parts of the component ie outside the return function where Context.Consumer is used exclusively. I assumed using the useContext hook would help with this as my component would be re-rendered with new values from context every time the div is clicked - however this is not the case. Any help figuring out why this is would be appreciated.

TL;DR: <Context.Consumer> updates and reflects changes to the context from child component, useContext does not although the component needs it to.

UPDATE: I have now figured out that useContext will read from the default context value passed to createContext and will essentially operate independently of Context.Provider . That is what is happening here, Context.Provider includes a method that modifies state whereas the default context value does not. My challenge now is figuring out a way to include a function in the default context value that can modify other properties of that value. As it stands:

const defaultContextValue = {
  data: {
    filterBy: 'year',
    isOptionClicked: false,
    filterValue: ''
  },
  set: () => {}
}

set is an empty function which is defined in the ContextProviderComponent (see above). How can I (if possible) define it directly in the context value so that:

const defaultContextValue = {
  data: {
    filterBy: 'year',
    isOptionClicked: false,
    filterValue: ''
  },
  test: 'hi',
  set: (newData) => {
     //directly modify defaultContextValue.data with newData
  }
}

There is no need for you to use both <Context.Consumer> and the useContext hook.

By using the useContext hook you are getting access to the value stored in Context.

Regarding your specific example, a better way to consume the Context within your Visuals component would be as follows:

import React, { useContext } from "react";
import Context from "./context";

const Visuals = () => {
  const filterByYear = "year";
  const filterByTheme = "theme";

  const { data, set } = useContext(Context);
  const { filterBy, isOptionClicked, filterValue } = data;

  const works =
    filterBy === filterByYear
      ? "filter nodes by year"
      : "filter nodes by theme";

  return (
    <div noFooter="true">
      <div>
        {data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1>}
        the value for the 'works' variable is: {works}
        <button onClick={() => set({ filterBy: "theme" })}>
          Filter by theme
        </button>
        <button onClick={() => set({ filterBy: "year" })}>
          Filter by year
        </button>
      </div>
    </div>
  );
};

export default Visuals;

Also, it seems that you are not using the works variable in your component which could be another reason for you not getting the desired results.

You can view a working example with the above implementation of useContext that is somewhat similar to your example in this sandbox

hope this helps.

Problem was embarrassingly simple - <Visuals> was higher up in the component tree than <Layout was for some reason I'm still trying to work out. Marking Itai's answer as correct because it came closest to figuring things out giving the circumstances

In addition to the solution cited by Itai, I believe my problem can help other people here

In my case I found something that had already happened to me, but that now presented itself with this other symptom, of not re-rendering the views that depend on a state stored in a context.

This is because there is a difference in dates between the host and the device. Explained here: https://github.com/facebook/react-native/issues/27008#issuecomment-592048282

And that has to do with the other symptom that I found earlier: https://stackoverflow.com/a/63800388/10947848

To solve this problem, just follow the steps in the first link, or if you find it necessary to just disable the debug mode

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM