为啥在 React 中,当父组件重新渲染时子组件不会重新渲染(子组件没有被 React.memo 包裹)?

Posted

技术标签:

【中文标题】为啥在 React 中,当父组件重新渲染时子组件不会重新渲染(子组件没有被 React.memo 包裹)?【英文标题】:Why, in React, do children not re-render when parent component re-renders(children are not wrapped by React.memo)?为什么在 React 中,当父组件重新渲染时子组件不会重新渲染(子组件没有被 React.memo 包裹)? 【发布时间】:2021-11-24 01:04:59 【问题描述】:

在React Hooks - Understanding Component Re-renders这篇文章中,我了解到当我们在父组件中使用useContextHook 时,只有消耗上下文的子组件会重新渲染。 并且文章给出了上下文的两种消费方式。看看sn-p:

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="" />
    </>
  );

低效使用useContext示例中,child组件TickerComponent (2)parent后未使用上下文重新渲染&lt;App /&gt; 使用上下文并重新渲染。但是在 高效使用 useContext 示例中,child TickerComponent (2) 没有重新渲染,即使它是 parent &lt;ThemeContxtProvider&gt; 重新渲染因为上下文的消耗。

我了解到,没有 React.memo 的子级在父级重新渲染时会重新渲染,那么为什么在 useContext 的高效消费示例中不会发生这种情况?

【问题讨论】:

【参考方案1】:

你的问题是你正在考虑像这样的代码

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>
  );

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>
  );

等效。虽然它们做同样的事情,并且或多或少地以相同的方式表现,但第二个示例将只呈现一次ComponentToRender,即使在多次按下“增量”按钮之后也是如此。 (而第一个将在每次按下按钮时重新渲染。)

这个概念也适用于您的示例。您的“低效消耗”将触发 App 的重新渲染,并强制刷新该组件的每个直接子级。 “有效消费”没有,因为事实并非如此。在我的简化示例中,ComponentToRender 实际上是由App 渲染的,而不是Clicker。所以Clicker 的状态变化不会影响ComponentToRender(只是作为孩子传递)

在第二个示例中,App 的另一种写法是:

function App() 
  const componentToRenderWithinApp = <ComponentToRender />

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

这个相当于&lt;Clicker&gt;&lt;ComponentToRender /&gt;&lt;/Clicker&gt;

【讨论】:

感谢您的回答!但是恕我直言,ComponentToRender 作为孩子粘贴到Clicker,那么为什么Clicker 的状态变化不会影响ComponentToRender?那是我无法理解的。 !draft 在你的图片中,ComponentToRender 实际上并不是 Clicker > div 的直接子级,它实际上是上一层。您应该将它放在树表示中的 App 下。 ComponentToRender作为children prop粘贴到Clicker,为什么要放在App下面呢? 不同之处在于创建ComponentToRender的是App。位置无关紧要(即,我传递了要在点击器中呈现的元素)。 Clicker 中的状态更改不需要重新渲染,因为 ComponentToRender 要求重新渲染的唯一方法是让 App 更改

以上是关于为啥在 React 中,当父组件重新渲染时子组件不会重新渲染(子组件没有被 React.memo 包裹)?的主要内容,如果未能解决你的问题,请参考以下文章

为啥组件在状态更改后不重新渲染。在 react-native 函数组件中

React 啥时候重新渲染子组件?

解决vue开发时子组件数据和组件渲染的异步问题

React.memo()、useCallback()、useMemo() 区别及基本使用

React 组件中的错误导致应用程序重新渲染,从而导致无限循环。为啥?

React,是不是在父级上渲染会使子级重新渲染?