52

I have been trying to understand the new React Context API and was playing with it. I just wanted to check a simple case - what all re-renders when data to a Provider is updated.

Check this small example on Codesandbox

So, in my example, I have an App component - that has state something like this --

this.state = {
  number - A random number
  text - A static text
} 

I create a new React Context from here containing number and text from state and pass the values to two Consumers Number and Text.

So my assumption is if the random number updates, it will change the context and both the components should trigger re-render.

But in reality, the value is updating but no rerender is happening.

So, my question -

  1. Are updated to the context not propagated via the ususal rerenders? As I cannot see my logs / color changes when context changes.

  2. Are all the consumers to that Provider updated or not?

Kamal
  • 2,482
  • 2
  • 33
  • 47
Sachin
  • 2,392
  • 2
  • 13
  • 28

2 Answers2

44

Are updated to the context not propagated via the ususal rerenders? As I cannot see my logs / color changes when context changes.

The updates to context values doesn't trigger re-render for all the children of the provider, rather only components that are rendered from within the Consumer, so in your case although number component contains the Consumer, Number component isn't re-rendered, rather just the render function within the Consumer and hence the value changes on context updates. This way it is quite a lot performant as it doesn't trigger re-renders for all of its children.

Are all the consumers to that Provider updated or not ?

All consumers to that Provider will go through an update cycle but whether or not they re-render is decided by the react virtual DOM comparison. A demo of this you can see in the console for this sandbox

EDIT

What you need to make sure is that the components are rendered as children of the ContextProvider component and you are passing handlers to it instead of rendering them inline and updating the state of ContextProvider because that will trigger a re-render of all components that are within the ContextProvider

Performant usage

App.js

  constructor() {
    super();
    this.state = {
      number: Math.random() * 100,
      text: "testing context api"
      updateNumber: this.updateNumber,
    };
  }
  render() {
    return (
      <AppContext.Provider
        value={this.state}
      >
        {this.props.children}
      </AppContext.Provider>
    );
  }

index.js

class Data extends React.Component {
  render() {
    return (
      <div>
        <h1>Welcome to React</h1>
        <Number />
        <Text />
        <TestComp />
        <AppContext.Consumer>
          {({ updateNumber }) => (
            <button onClick={updateNumber}>Change Number </button>
          )}
        </AppContext.Consumer>
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(
  <App>
    <Data />
  </App>,
  rootElement
);

Less Performant usage

App.js

class App extends Component {
  constructor() {
    super();
    this.state = {
      number: Math.random() * 100,
      text: "testing context api"
    };
  }

  updateNumber = () => {
    const randomNumber = Math.random() * 100;
    this.setState({ number: randomNumber });
  };

  render() {
    return (
      <AppContext.Provider value={this.state}>
        <div>
          <h1>Welcome to React</h1>
          <Number />
          <Text />
          <TestComp />
          <button onClick={this.updateNumber}>Change Number </button>
        </div>
      </AppContext.Provider>
    );
  }
}
Shubham Khatri
  • 211,155
  • 45
  • 305
  • 318
  • I'm interested in this as I have a similar issue. To me because there are 2 console.logs and only one item changing - I think there are still 2 renders going on and I would think one of them is unnecessary. My thinking was that only one Item should update and thus one console.log - why isnt that the case here? And how would you achieve that outcome? – Spencer Bigum Jul 08 '18 at 14:51
  • 1
    why update state inline can trigger re-render all child components and separate it will not? – TomSawyer Oct 24 '19 at 05:17
  • 2
    @TomSawyer, That is because when you render them inline, they are within the hierarchy of your Provider component and will re-render on state change of provider – Shubham Khatri Oct 24 '19 at 05:21
  • 2
    what about useContext? – Reza Sam Jan 07 '20 at 12:22
22

Here is an update for your questions based on the useContext Hook:

const value = useContext(MyContext)

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. If re-rendering the component is expensive, you can optimize it by using memoization.

So given below code example, components Number and Text will re-render with each context value change, as both directly contain useContext(AppContext).

const AppContext = React.createContext();

const Number = React.memo(props => {
  const renderCount = useRenderCount();
  const contextNo = React.useContext(AppContext);
  return (
    <div style={{ backgroundColor: `${randomColor()}` }}>
      Number: rendered {renderCount.current} times.
    </div>
  );
});

const Text = React.memo(() => {
  const renderCount = useRenderCount();
  const context = React.useContext(AppContext);
  return (
    <div style={{ backgroundColor: `${randomColor()}` }}>
      Text: rendered {renderCount.current} times. I rerender with context value
      changes!
    </div>
  );
});

const App = () => {
  const [ctxVal, setCtxVal] = React.useState(0);
  const [prop, setProp] = React.useState(0);
  return (
    <AppContext.Provider value={ctxVal}>
      <Number prop={prop} />
      <Text />
      <button onClick={() => setCtxVal(ctxVal + 1)}>
        Change context value
      </button>
      <button onClick={() => setProp(prop + 1)}>
        Only change prop in Number
      </button>
    </AppContext.Provider>
  );
};

function useRenderCount() {
  const renderCount = React.useRef(1);
  React.useEffect(() => {
    renderCount.current += 1;
  }); 
  return renderCount;
}

function randomColor() {
  const letters = "0123456789ABCDEF"; let color = "#";
  for (let i = 0; i < 6; i++) color += letters[Math.floor(Math.random() * 16)];
  return color;
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
Community
  • 1
  • 1
ford04
  • 30,106
  • 4
  • 111
  • 119
  • 1
    Extension of @ford04's example using object as ctxVal is here: [codesandbox](https://codesandbox.io/s/naughty-glade-okift?file=/src/App.js). Thanks author! I needed to quickly show an example of how object change detection worked, and was able to quickly use your example as base. – John Lee Jun 05 '20 at 05:07
  • you mention that it doesn;t care if the parent have should component or React,memo . but in my case I memorize the parent where I use useContext and when the value in contextapi changed it doesn't rerender .. – Bryan Lumbantobing Feb 03 '21 at 13:23
  • 2
    @BryanLumbantobing Rerendering is triggered for the component that uses `useContext` in case of value changes. If your child component is wrapped by `React.memo`, and its props haven't changed, then this child won't rerender despite parent context change. – ford04 Feb 03 '21 at 14:09
  • How come the `Text` component updates its state based on the `context` if the `context const` is only declared but not used in the return method? is it enough to declare the context inside the component for the re-rending to take place? Shouldn't `context.someVariable` then be used in the return method for its changes to then be propagated to this `Text` components once the state updates? – FedericoCapaldo Apr 20 '21 at 10:42
  • 1
    @FedericoCapaldo yes, it is sufficient for a component to contain `useContext` ro rerender when the context value changes. – ford04 Apr 23 '21 at 07:29