1

I think the general rule is: the context consumers only re-render when the context value has changed. The rule:

https://reactjs.org/docs/context.html#contextprovider

All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes. The propagation from Provider to its descendant consumers (including .contextType and useContext) is not subject to the shouldComponentUpdate method, so the consumer is updated even when an ancestor component skips an update.

Changes are determined by comparing the new and old values using the same algorithm as Object.is.

However, on this sample app, https://codesandbox.io/s/loving-wood-dzqm9g (to be able to see the app running, you may need to bring it to its own window: https://dzqm9g.csb.app/ ), every component re-renders regardless the context value has changed or not.

The first Context.Provider actually changes the value:

      <ThemeContext.Provider
        value={toggle ? themeColors.light : themeColors.dark}
      >

so it is reasonable that the consumer underneath it is re-rendered. But the <TheTimeNow /> is not a consumer, and is re-rendered for some reason. The time is updated in the box every time the Toggle button is pressed.

And even the second Context.Provider:

      <ThemeContext.Provider value={themeColors.light}>
        <div
          className="App"
          style={{ border: "3px dotted #07f", margin: "18px" }}
        >
          <Container />
        </div>

        <TheTimeNow />
      </ThemeContext.Provider>

The context value does not change at all, yet all the components are re-rendered.

To make it one step further, I outright provided a constant context value and use only one context provider:

https://codesandbox.io/s/cranky-sunset-3sbhr4?file=/src/App.js

And I am still able to make every component re-render, as we can see the time updated in every component.

So is it against the rule mentioned at the top of this post? Or is it true that we also have this rule: every children component re-renders when the parent re-renders (and therefore the whole subtree re-renders ? So because <App /> re-renders, everything underneath re-renders?

So when both rules apply, then it ended up all children in the subtree re-renders.

However, how would we make a <Context.Provider> change its value, but have this container component not re-render? I cannot think of a case because we usually use a state or props to cause the context value to change and re-render this component, so the whole subtree will re-render. How is it possible the value change but this container doesn't re-render?

In other words, for <ThemeContext.Provider value={someValue}> to give a different value, this <ThemeContext.Provider> component must be rendering and therefore, the component containing this line is also re-rendering, and so all children, and even the whole subtree would re-render. Then why would we say, "only the context consumers will re-render"?

nonopolarity
  • 146,324
  • 131
  • 460
  • 740
  • Remove web-vitals from dependencies in the code sandbox. – super Aug 07 '22 at 18:37
  • It seems you are making a lot of assumptions about how react works under the hood and how that should impact what you see. I'm don't mean to say you are right or wrong, but after reading your question it's not very clear if your reasoning is infact sound. A parent updated state. A child to that parent was checked and actually produces different end result then previous render, so it's re-rendered. Not sure how that in any way goes against your initial quote... – super Aug 07 '22 at 18:41
  • @super I removed it and it is the same behavior. Couldn't you have forked a copy and remove it and try it in 30 seconds? – nonopolarity Aug 07 '22 at 19:02
  • @super making assumptions? Those are docs or a well accepted answer on Stackoverflow and everybody agreed: when parent component re-renders, in general all children re-render. These are all facts – nonopolarity Aug 07 '22 at 19:03
  • I did. I removed it and it worked for me. https://codesandbox.io/s/goofy-sky-wywxnq – super Aug 07 '22 at 19:06
  • @super what are you talking about? Your codesandbox sample just works the same way like mine: everything re-renders – nonopolarity Aug 07 '22 at 19:08
  • Oh, it had nothing to do with the behaviour. Yours didn't load at all cause it failed to install web-vitals dependency. Mine actually works for me. – super Aug 07 '22 at 19:09
  • @super I am not sure why it didn't work for you but it works on my side. But you are "making it work" which is totally irrelevant to what this question is about – nonopolarity Aug 07 '22 at 19:11
  • So the obvious answer here is that either the context-provider updates itself, or one of it's parents update. That's the only way to change the value. So that kind of leaves your question a bit unclear... ? – super Aug 07 '22 at 19:24
  • a person who is pretty good at React can answer my question. You obviously don't quite understand what is happening – nonopolarity Aug 07 '22 at 19:34
  • You obviously don't deserve my time. Good luck. – super Aug 07 '22 at 19:51
  • do not write an answer that is irrelevant to the question... it appeared you are in it to get some reputation but not for the tech – nonopolarity Aug 07 '22 at 20:58
  • Wow... just wow. – super Aug 07 '22 at 21:14

2 Answers2

2

Your toggle state is managed at the app root, which means whenever any children update that state, the entire app re-renders:

function App() {
  // This state is managed _outside_ the ThemeContext
  const [toggle, setToggle] = useState(true);
  return (
    <div>
      <ThemeContext.Provider
        value={toggle ? themeColors.light : themeColors.dark}
      >
    ...

ThemeContext.Provider is consuming the state from the App, but you want the state to be managed locally to the Provider. If you create a ThemeProvider component to encapsulate the theme state, I think you'll have what you're looking for: only the consumers update when the theme changes.

I wasn't able to fork your CodeSandbox for some reason, but created a simple example on StackBlitz: https://stackblitz.com/edit/react-ts-286rhj?file=App.tsx

Here's the relevant code:

export const ThemeContext = React.createContext();

export function ThemeProvider({ children }) {
  // State changes here only affect context consumers
  const [toggle, setToggle] = React.useState(true);

  const theme = toggle ? 'light' : 'dark';
  const toggleTheme = () => setToggle((t) => !t);

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

ConsumingChild contains useContext(ThemeContext), and updates when toggle changes. NonConsumingChild doesn't consume context, and doesn't re-render when toggle changes.

function App() {
  return (
    <ThemeProvider>
      <ConsumingChild />
      <NonConsumingChild />
    </ThemeProvider>
  )
}

Edit: If you want a button to change the theme and not re-render anything around it, you can create a Button component which can go anywhere in your app as long as it's within the ThemeProvider:

export function ThemeButton() {
  const {toggleTheme} = useContext(ThemeContext)

  return (
    <button onClick={toggleTheme}>
      Toggle the theme
    </button>
  )
}

And here's the consuming child:

export function ConsumingChild() {
  const {theme} = useContext(ThemeContext);

  return (
    <>
      <div>The current theme is {theme}</div>
      <ThemeButton />
    </>
  )
}

Edit: As you mention in a comment, the ThemeProvider value will be a different reference on every render so all consumers will re-render when a change is made to any of them. There are ways to optimize this (for example see this article and this thread), but follow the advice in the article and make sure you need to optimize renders before going through the process, because your app might be as fast as needed without that optimization.

helloitsjoe
  • 6,264
  • 3
  • 19
  • 32
  • Yes i saw this in the docs in the form of a class component. This is a better solution! – Casper Kuethe Aug 07 '22 at 22:05
  • that's somewhat mind blowing. You put the state into the `ThemeProvider` and then pass the toggle function out as part of the context? So I was able to create what you said in my example: https://codesandbox.io/s/busy-shockley-35fxhx and to see it full screen: https://35fxhx.csb.app/ However, wherever I want to play that button, I have to make that whole component and subtree re-render. I cannot place the button in `App` now because it is not part of the context consumer. – nonopolarity Aug 08 '22 at 00:25
  • If I place that button inside of `App`, I have to make `App` a consumer, and viola! I am back to square 1: the whole subtree of `App` needs to re-render because now `App` is a consumer... also, even if I always provide the `light` theme, the consumer still re-renders because now context value is a new object every time due to the object literal`{ colors: themeColors.light, toggleIt }` ... is there any good solution? I can understand why some MIT guy said he was hired into doing the job of a configurator, not a programmer – nonopolarity Aug 08 '22 at 00:27
  • I've made a few edits which should hopefully clarify some things, but in brief: 1. You can consume the context as close as you need to the button to avoid re-rendering other parts of the subtree. 2. Yes, the ThemeProvider's value is an object literal causing rerenders to all context consumers. There are ways to optimize this if needed (e.g. create a stable reference to the object), but make sure it's needed. – helloitsjoe Aug 08 '22 at 01:11
  • Does that answer your question? – helloitsjoe Aug 09 '22 at 01:35
1

In your case the time component is updating because its parent component (the App component) state changes when you click the button. This triggers a rerender of the whole component as usual in React.

You are wrong about your assumption that context consumers only rerenders when the context has changed. Two paragraphs above in the React docs:

Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes.

This means that every child component of the context provider is subscribed to context changes. This means that every child component no matter how deep the level will update when the context updates.

React considers all descendent components of the context providers to be consumers not only the components that use useContext!

I think you are confused by this sentence:

The propagation from Provider to its descendant consumers (including .contextType and useContext) is not subject to the shouldComponentUpdate method, so the consumer is updated even when an ancestor component skips an update.

This only means that descendent components of a component where the shouldComponentUpdateMethod returns false (implying that the component should not be updated) are still being updated with the new value of the context.

If you are looking for a way to prevent a component from updating when none of it props change (the same prop values will always produce the same output) then have a look at React Memo Components.

In your provided codesandbox if you refactor the code of TheTimeNow.jsx to be a Memo component you will see that the time won't get updated when you press the toggle button, but the colors will change.

import React from "react";

export default React.memo(function TheTimeNow() {
  return (
    <h2>TheTimeNow component says it is {new Date().toLocaleTimeString()}</h2>
  );
});
Casper Kuethe
  • 1,070
  • 8
  • 13
  • that's because there is `` and I would think that labels it as a context consumer. You are saying without that, it is still a consumer? That's a bit weird and confusing – nonopolarity Aug 07 '22 at 21:35
  • The context will always be passed through all the descendent component `` is just the way in React to acces a context in a function component. And yes the word consumer is a bit ambiguous in my opinion. – Casper Kuethe Aug 07 '22 at 21:51
  • But aside from if or if not all descendent components or just the components with the `` component are subscribed to the changes, in your case the parent component props of the provider changes which causes the rerender of everything. – Casper Kuethe Aug 07 '22 at 21:53