14

In another StackOverflow post, Nicholas helped me understand that a Context.Provider re-renders its descendant Context.Consumer components when the context value the Provider provides changes.

That is further confirmed by the official Doc:

All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes.

Nicholas also helped me understand that the only way a Provider will know if the context value has changed, is if its enclosing component re-renders.

In summary:

  1. Providers update its Consumers whenever the context value changes
  2. This can only happen when the enclosing function around the Provider re-renders
  3. Which leads to the Provider and all its descendants re-rendering anyways

Thus, the feature in (1) above, seem redundant. If Provider ever only updates Consumers when its enclosing component re-renders, and spotting a context value update can only happen when the parent re-renders, there is no need to have the feature that allows the Provider to update the Consumers when the context value changes.

What am I missing here?


EDIT

Nicholas also says in a comment:

App can (conceivably) rerender due to things that have nothing to do the value its providing via context. If this happens, you do not want the consumers to rerender. For that to happen, you need the value before and the value after to pass a === check. If you're providing an object, that means you can't create a brand new object in App's render method, or you'll end up rerendering consumers unnecessarily.

However, I was under the impression that when a parent re-renders, all its children will also re-render. Thus, the === check mentioned above would not help, i.e. the children would re-render regardless.

Magnus
  • 6,791
  • 8
  • 53
  • 84

2 Answers2

8
  1. Which leads to the Provider and all its descendants re-rendering anyways

While this is the default behavior, in practice it's common to change this in order to improve performance. Pure components, components that implement shouldComponentUpdate, or components using React.memo will cause the rerendering to stop before going through the entire tree.

For example: suppose there's a toplevel component with some state, which renders a midlevel component that has shouldComponentUpdate() { return false; }, which renders a bottom level component.

function TopLevelComponent() {
  const [topLevelState, setTopLevelState] = useState(0);
  return (
    <>
      <h1>Top Level Component</h1>
      <button onClick={setTopLevelState( v => v + 1)}>Update Top Level State</button>
      <MidLevelComponent />
    </>
  );
}
class MidLevelComponent extends React.Component {
  shouldComponentUpdate() {
    return false; // <= This guy will prevent re-rendering of this component and everything nested under it
  }
  render() {
    return (
      <>
        <h2>Mid Level Component</h2>
        <BottomLevelComponent />
      </>
    );
  }
}
function BottomLevelComponent() {
  return "Bottom Level";
}

On the initial mount, all 3 of these will render. But then if the toplevel component updates its state, only the toplevel component will rerender. The midlevel component will be skipped due to its shouldComponentUpdate, and then the bottom level component is never even considered. (See live code snippet below -better run in Full Page mode)

console.log("--- Initial Render");

function BottomLevelComponent() {
  console.log("BottomLevelComponent() => renders");
  return "Bottom Level";
}

class MidLevelComponent extends React.Component {
  shouldComponentUpdate() {
    return false;
  }
  render() {
    console.log("MidLevelComponent() => renders");
    return (
      <div>
        <h2>Mid Level Component</h2>
        <BottomLevelComponent />
      </div>
    );
  }
}

function TopLevelComponent() {
  console.log("TopLevelComponent() => renders");
  const [topLevelState, setTopLevelState] = React.useState(0);
  const handleTopLevelUpdate = () => {
    console.log("--- Updating Top Level State");
    setTopLevelState((v) => v + 1);
  };
  return (
    <div>
      <h1>Top Level Component</h1>
      <button onClick={handleTopLevelUpdate}>Update Top Level State</button>
      <MidLevelComponent />
    </div>
  );
}

ReactDOM.render(<TopLevelComponent />, document.getElementById("root"));
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Now we add in a context provider to the toplevel component, and a context consumer to the bottom level component. On the initial mount, they will again all render. If the toplevel component updates its state, it will rerender. The midlevel component will still skip its render, due to its shouldComponentUpdate. But as long as the context value changed, the bottom level component will rerender, even though its parent bailed out. This is the feature that is referred to by that blurb.

const TopLevelContext = React.createContext();

export default function TopLevelComponent() {
  const [topLevelState, setTopLevelState] = useState(0);
  return (
    <TopLevelContext.Provider value={{ topLevelState }}>
      <h1 onClick={setTopLevelState((v) => v + 1)}>Top Level Component</h1>
      <MidLevelComponent />
    </TopLevelContext.Provider>
  );
}
class MidLevelComponent extends React.Component {
  shouldComponentUpdate() {
    return false; // <= Will prevent rendering of this Component and everything nested under it, but...
  }
  render() {
    return (
      <>
        <h2>Mid Level Component</h2>
        <BottomLevelComponent />
      </>
    );
  }
}
function BottomLevelComponent() {
  React.useContext(TopLevelContext); // <= ...this will override the shouldComponentUpdate of the parent and trigger a re-render when the Context provider value changes
  return "Bottom Level";
}

    console.log("--- Initial Render");
    
    const TopLevelContext = React.createContext();

    function BottomLevelComponent() {
      React.useContext(TopLevelContext);
      console.log("BottomLevelComponent() => renders");
      return "Bottom Level";
    }

    class MidLevelComponent extends React.Component {
      shouldComponentUpdate() {
        return false;
      }
      render() {
        console.log("MidLevelComponent() => renders");
        return (
          <div>
            <h2>Mid Level Component</h2>
            <BottomLevelComponent />
          </div>
        );
      }
    }

    function TopLevelComponent() {
      console.log("TopLevelComponent() => renders");
      const [topLevelState, setTopLevelState] = React.useState(0);
      const handleTopLevelUpdate = () => {
        console.log("--- Updating Top Level State");
        setTopLevelState((v) => v + 1);
      };
      return (
        <TopLevelContext.Provider value={{ topLevelState }}>
          <h1>Top Level Component</h1>
          <button onClick={handleTopLevelUpdate}>Update Top Level State</button>
          <MidLevelComponent />
        </TopLevelContext.Provider>
      );
    }

    ReactDOM.render(<TopLevelComponent />, document.getElementById("root"));
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Kostas Minaidis
  • 4,681
  • 3
  • 17
  • 25
Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98
  • that is interesting, as I was facing this issue where a deeply nested component (X) had to be changed ... and because i was `prop-drilling` the changed prop all the way down to X it was causing all the components in its way to re-render. However now if I get you right .. it seems that if I pass the `changing` props via `context` all the in-between components can get away from being re-rendered.. am I right ? – user3124360 Feb 07 '22 at 19:00
  • 1
    Yes, it's possible to skip rendering the in between components. At least one of the inbetween components will need to use shouldComponentUpdate/PureComponent (for class components) or memo (for function components). – Nicholas Tower Feb 07 '22 at 19:15
0

This behavior isn't really new. When a react component receives a different set of props, (assuming the shouldComponentUpdate() returns true) it will re-render its children, and the effect will cascade down.

So, if you have a setup like this:

<ParentComponent>
  <Provider>
    <Context>
      <ChildReceivingContext />
    </Context>
  </Provider>
</ParentComponent>

If Provider ever only updates Consumers when its enclosing component re-renders,

When parent component re-renders, the effect will cascade downwards, and components will update if shouldComponentUpdate() return true.

there is no need to have the feature that allows the Provider to update the Consumers when the context value changes.

What am I missing here?

I believe what you're missing is that there is no such feature. That's just how react works.

The actual feature from Provider/Consumer, is the fact that you don't have to manually pass down props from parent to child to sub child etc, but you can have a children deep down to be enclosed by a provider and receive the prop directly.

Christopher Francisco
  • 15,672
  • 28
  • 94
  • 206