1

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 i.e 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
  }
}
divergent
  • 271
  • 4
  • 17
  • 1
    Keep in mind that default context value is only going to be used if there is no context provider in the consumer component ancestors. – ichigolas Jan 20 '20 at 18:03
  • 1
    @nicooga am I right in thinking that in this case default context value is passed as the value to the context provider and is therefore used regardless – divergent Jan 20 '20 at 18:29
  • 1
    Yes you are right, but it is redundant. You also don't need to wrap your elements in `Context.Consumer` if you are using `useContext`. Check this example: https://codesandbox.io/s/react-context-usage-example-04vkj. – ichigolas Jan 20 '20 at 18:32
  • ***"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*** - the usual pattern is to update Context with `useReducer()` [ref](https://medium.com/@seantheurgel/react-hooks-as-state-management-usecontext-useeffect-usereducer-a75472a862fe), although your set function approach might work. – Richard Matsen Jan 21 '20 at 07:02

3 Answers3

5

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.

Itai
  • 191
  • 5
  • Thanks for taking the time to answer. I now understand that using both `Context.Consumer` and `useContext` is redundant. However the fundamental problem remains that the component is not re-rendered on context state change. – divergent Jan 20 '20 at 19:41
  • I believe that the issue might originate elsewhere in your code. if you could reproduce your error in a sandbox it would be easier to find the exact problem that is causing you trouble. If you take a look at the sandbox I shared you could see that the component rerenders every time the context changes. – Itai Jan 20 '20 at 19:45
  • Here's a sandbox: https://codesandbox.io/s/fervent-waterfall-e9s8b?fontsize=14&hidenavigation=1&theme=dark. Currently working through yours as well – divergent Jan 20 '20 at 20:03
  • It seems that `isOptionsClicked` is always false so you are always rendering you `` component and that `filterValue` is always an empty string so your `works` array is always empty. This isn't an issue with context but with the implementation of the component's logic. I can't really give any more insight without seeing the full repo. Sorry. – Itai Jan 20 '20 at 20:26
  • That's alright. Really appreciate your help. I had this component working perfectly but I needed state to persist to other pages hence me rewriting it. – divergent Jan 20 '20 at 20:59
  • This is still not functioning as intended. Changing a value in the context does not trigger a re-render. If something else causes a render then you will see a new value (after the `set()` function is actually populated). If you use `useState()` it will cause a re-render . https://codesandbox.io/s/sharp-beaver-gvk2g – Mikhail Jan 13 '21 at 16:07
  • @Itai The sandbox link is broken. Mind fixing it. – Tafadzwa Gonera Feb 23 '21 at 15:00
0

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

divergent
  • 271
  • 4
  • 17
-1

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