Search code examples
javascriptreactjsreact-hooksgatsbyreact-context

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 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
  }
}

Solution

  • 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.