2

In this article React Hooks - Understanding Component Re-renders, I learned that when we use useContext Hook in parent component, only the children components which consume the context would re-render.

And the article gives two way of consumptions of context. Take a look at the snippet:

Efficient consumption of useContext\

import React from "react";
import ReactDOM from "react-dom";
import TickerComponent from "./tickerComponent";
import ThemedTickerComponent from "./themedTickerComponent";
import { ThemeContextProvider } from "./themeContextProvider";
import ThemeSelector from "./themeSelector";

import "./index.scss";
import logger from "./logger";

function App() {
  logger.info("App", `Rendered`);
  return (
    <ThemeContextProvider>
      <ThemeSelector />
      <ThemedTickerComponent id={1} />
      <TickerComponent id={2} />
    </ThemeContextProvider>
  );
}
import React, { useState } from "react";

const defaultContext = {
  theme: "dark",
  setTheme: () => {}
};

export const ThemeContext = React.createContext(defaultContext);

export const ThemeContextProvider = props => {
  const setTheme = theme => {
    setState({ ...state, theme: theme });
  };

  const initState = {
    ...defaultContext,
    setTheme: setTheme
  };

  const [state, setState] = useState(initState);

  return (
    <ThemeContext.Provider value={state}>
      {props.children}
    </ThemeContext.Provider>
  );
};
import React from "react";
import { useContext } from "react";
import { ThemeContext } from "./themeContextProvider";

function ThemeSelector() {
  const { theme, setTheme } = useContext(ThemeContext);
  const onThemeChanged = theme => {
    logger.info("ThemeSelector", `Theme selection changed (${theme})`);
    setTheme(theme);
  };
  return (
    <div style={{ padding: "10px 5px 5px 5px" }}>
      <label>
        <input
          type="radio"
          value="dark"
          checked={theme === "dark"}
          onChange={() => onThemeChanged("dark")}
        />
        Dark
      </label>
      &nbsp;&nbsp;
      <label>
        <input
          type="radio"
          value="light"
          checked={theme === "light"}
          onChange={() => onThemeChanged("light")}
        />
        Light
      </label>
    </div>
  );
}

module.exports = ThemeSelector;
import React from "react";
import { ThemeContext } from "./themeContextProvider";
import TickerComponent from "./tickerComponent";
import { useContext } from "react";

function ThemedTickerComponent(props) {
  const { theme } = useContext(ThemeContext);
  return <TickerComponent id={props.id} theme={theme} />;
}

module.exports = ThemedTickerComponent;
import React from "react";
import { useState } from "react";
import stockPriceService from "./stockPriceService";
import "./tickerComponent.scss";

function TickerComponent(props) {
  const [ticker, setTicker] = useState("AAPL");
  const currentPrice = stockPriceService.fetchPricesForTicker(ticker);
  const componentRef = React.createRef();

  setTimeout(() => {
    componentRef.current.classList.add("render");
    setTimeout(() => {
      componentRef.current.classList.remove("render");
    }, 1000);
  }, 50);

  const onChange = event => {
    setTicker(event.target.value);
  };

  return (
    <>
      <div className="theme-label">
        {props.theme ? "(supports theme)" : "(only dark mode)"}
      </div>
      <div className={`ticker ${props.theme || ""}`} ref={componentRef}>
        <select id="lang" onChange={onChange} value={ticker}>
          <option value="">Select</option>
          <option value="NFLX">NFLX</option>
          <option value="FB">FB</option>
          <option value="MSFT">MSFT</option>
          <option value="AAPL">AAPL</option>
        </select>
        <div>
          <div className="ticker-name">{ticker}</div>
          <div className="ticker-price">{currentPrice}</div>
        </div>
      </div>
    </>
  );
}

module.exports = TickerComponent;

Inefficient consumption of useContext

import React from "react";
import ReactDOM from "react-dom";
import { useContext } from "react";
import TickerComponent from "./tickerComponent";
import ThemedTickerComponent from "./themedTickerComponent";
import { ThemeContextProvider } from "./themeContextProvider";
import { ThemeContext } from "./themeContextProvider";

function App() {
  const { theme, setTheme } = useContext(ThemeContext);
  const onThemeChanged = theme => {
    setTheme(theme);
  };
  return (
    <>
      <div style={{ padding: "10px 5px 5px 5px" }}>
        <label>
          <input
            type="radio"
            value="dark"
            checked={theme === "dark"}
            onChange={() => onThemeChanged("dark")}
          />
          Dark
        </label>
        &nbsp;&nbsp;
        <label>
          <input
            type="radio"
            value="light"
            checked={theme === "light"}
            onChange={() => onThemeChanged("light")}
          />
          Light
        </label>
      </div>
      <ThemedTickerComponent id={1} />
      <TickerComponent id={2} theme="" />
    </>
  );
}

In the Inefficient consumption of useContext example, the child component TickerComponent (2) which didn't consume context re-rendered since the parent <App /> consumed context and re-rendered. But in Efficient consumption of useContext example, the child TickerComponent (2) didn't re-render even it's parent <ThemeContxtProvider> re-rendered because of consumption of context.

I learned that children without React.memo will re-render when parent re-render, so why in Efficient consumption of useContext example that not happen?

TORyTANG
  • 45
  • 1
  • 6

1 Answers1

1

Your problem is that you are considering code like

function ComponentToRender() {
  const count = React.useRef(0)

  React.useEffect(() => {
    console.log('component rendered', count.current++)
  })

  return null
}

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>You clicked {count} times!</h2>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ComponentToRender />
    </div>
  );
}

and

function ComponentToRender() {
  const count = React.useRef(0)

  React.useEffect(() => {
    console.log('component rendered', count.current++)
  })

  return null
}

function Clicker({ children }) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>You clicked {count} times!</h2>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      {children}
    </div>
  );
}

function App() {
  return (
    <Clicker>
      <ComponentToRender />
    </Clicker>
  );
}

equivalent. While they do the same thing, and behave more or less in the same way, the second example will render ComponentToRender only once, even after pressing the "increment" button multiple times. (while the first one will re-render each time the button is pressed.)

The concept apply to your example as well. Your "inefficient consumption" will trigger a re-render from App, and force a refresh to every direct child of that component. The "efficient consumption" doesn't, because that's not the case. In my simplified example, ComponentToRender is actually rendered by App, not Clicker. So a change in state of Clicker will not impact ComponentToRender (that was just passed as children)

Another way for App to be written, in the second example, is:

function App() {
  const componentToRenderWithinApp = <ComponentToRender />

  return (
    <Clicker>
      {componentToRenderWithinApp}
    </Clicker>
  );
}

this one is equivalent to <Clicker><ComponentToRender /></Clicker>

Federkun
  • 36,084
  • 8
  • 78
  • 90
  • Thanks for your answer! But IMHO, `ComponentToRender` is pasted to `Clicker` as a children, so why a change in state of `Clicker` will not impact `ComponentToRender`? That's what I cannot understand. – TORyTANG Oct 03 '21 at 07:38
  • ![draft](https://cdn.jsdelivr.net/gh/tuchuang-ml/tuchuang@fb143d3ff87bb3bcccc2ce115f6c3c859a37d128/2021/10/03/f620ca9d9c9db11abe99ed4c44212998.png) – TORyTANG Oct 03 '21 at 07:49
  • In your image, ComponentToRender is not actually a direct children of Clicker > div, It's actually a level above. You should position it under App in you tree representation. – Federkun Oct 03 '21 at 07:57
  • ComponentToRender is pasted to Clicker as children prop, so why should position it under App? – TORyTANG Oct 03 '21 at 08:06
  • The difference is that it's App that created ComponentToRender. The position don't matter (i.e, that I passed that element to be rendered inside clicker). A change in state in clicker will not require a re-render, because the only way for ComponentToRender to require one is for App to change – Federkun Oct 03 '21 at 08:09
  • I wrote a code here: [sandbox](https://codesandbox.io/s/quirky-spence-us531?file=/src/App.js) And this is the Dom of the snippet [dom_pic](https://cdn.jsdelivr.net/gh/tuchuang-ml/tuchuang@19af338a6764b08a80731277ff569562bf7980cf/2021/10/03/0c03ca82d35deb67125cd18dbcefcb82.png) – TORyTANG Oct 03 '21 at 08:44
  • What matters is how react's fiber traverse the nodes, not the resulting dom. If you want a more in-depth explanation on why this happens, you may want to look for a "how react fiber work" articles (there are few quite complete) – Federkun Oct 03 '21 at 09:16
  • Thanks! It seems that i should dive into how react render components. Thank you for your help!!!! – TORyTANG Oct 03 '21 at 11:58