TORyTANG
TORyTANG

Reputation: 55

Why, in React, do children not re-render when parent component re-renders(children are not wrapped by React.memo)?

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?

Upvotes: 3

Views: 1439

Answers (1)

Federkun
Federkun

Reputation: 36964

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>

Upvotes: 1

Related Questions