Why does the component re-render after passing props.children in React?

After passing in props.children, why does it cause the component to be re-rendered?

Problem Description

In react, I wanted to optimize the rendering of components, and encountered a very interesting problem. When I passed props.children into a component, every time the parent component was re-rendered, the component would be re-rendered; It looks like a component wrapped in memo. The props and its own state have not changed, but the component has been re-rendered; I wrote a demo below, let’s take a look at this problem:

A Home component is introduced in the parent component App:

import Home from "./pages/Home";
import {
    
     useState } from "react";

function App() {
    
    
  const [count, setCount] = useState(0);
  console.log("App is render");

  return (
    <div className="App">
      {
    
    count}
      <button onClick={
    
    () => setCount(count + 1)}>Increment</button>
      <Home></Home>
    </div>
  );
}

Use memo to wrap the Home subcomponent, and the Home component can receive a props.children to display the components passed into Home, as follows:

import React, {
    
     memo } from "react";

const Home = memo((props) => {
    
    
  console.log("Home is render");
  return (
    <div>
      Home
      {
    
    props.children}
    </div>
  );
});

export default Home;

Currently, in the App component, props.children are not passed to the Home component. At this time, both the App component and the Home component will be re-rendered when it is loaded for the first time. When we click the Increment button to change the value of count, the App component will be re-rendered. Since the Home component is wrapped by memo, when the Home component's props and its own state have not changed, the component will not be re-rendered. This is currently what we expect, and there is no problem.

However, when we pass props.children to the Home component in the App component, problems will occur (This problem is not limited to the following example where I pass in an About Component, this problem will occur when passing in any element, even if we pass in a simple div element):

import {
    
     useState } from "react";
import Home from "./pages/Home";
import About from "./pages/About";

function App() {
    
    
  const [count, setCount] = useState(0);
  console.log("App is render");

  return (
    <div className="App">
      {
    
    count}
      <button onClick={
    
    () => setCount(count + 1)}>Increment</button>
      <Home>
        <About />
      </Home>
    </div>
  );
}

The About component is also wrapped in memo, and the code is as follows:

import React, {
    
     memo } from "react";

const About = memo(() => {
    
    
  console.log("About is render");
  return <div>About</div>;
});

export default About;

At this time, if we modify the value of count, it will cause the App component to be re-rendered, but it will also cause the Home component to be re-rendered. This is a bit confusing, let’s analyze it:

First of all, we know that without any optimization, re-rendering of the parent component will definitely lead to re-rendering of the child component, and a new component instance will be created; and if memo is used to wrap the component , then when the component's props and its own state have not changed, the parent component will not re-render the child component. Does this mean that a new component instance will not be created? (Here we have entered into a misunderstanding)

In the above code, we pass an About component to the Home component. The current performance in the Home component is equivalent to props.children = <About/>. Since the Home component is wrapped in memo and re-rendered, the most likely scenario is props have changed. The tangle is that there is only one attribute children in the props at this time, and the value is the About component. The About component is also wrapped by memo and does not rely on any props or state. If the result returned by the About component should be the same, it should not This will cause the props of the Home component to change.

This is the problem I encountered, Why does props.children affect the rendering of the component?

problem analysis

I still suspect that the props of the Home component have changed. The only thing that may have changed is the About component. In order to verify my idea, I defined an aboutRef variable in the Home component and used useRef to wrap the About component, as shown below:

import Home from "./pages/Home";
import {
    
     useState } from "react";

function App() {
    
    
  const [count, setCount] = useState(0);
  // 使用useRef包裹
  const aboutRef = useRef(<About/>);
  console.log("App is render");

  return (
    <div className="App">
      {
    
    count}
      <button onClick={
    
    () => setCount(count + 1)}>Increment</button>
      <Home>{
    
    aboutRef.current}</Home>
    </div>
  );
}

At this point I discovered that App, Home, and About would all be rendered when rendering for the first time, and when count changed, only the App component would be re-rendered, which achieved the effect I originally expected. But why can this effect be achieved by wrapping useRef? At this point, it has been determined that the props.children of the Home component must have changed, so let's explore why the About component has changed.

The reason why changes is because each time the component is re-rendered, a React element will be created, such as <About /> = jsx(About), and a new object will be returned when called. Of course, it is not just About that will be created like this. Other components and elements are also created this way. Among them, jsx() is just syntactic sugar for React.createElement. Elements or components will be created through React.createElement and return a ReactElement object. This is because React uses ReactElement objects to form a Javascript object tree (also It's the virtual DOM). I entered into a misunderstanding earlier, thinking that components wrapped in memo will not be recreated. In fact, regardless of whether there is a memo package, it will pass. React.createElement to create, but the React elements created by components wrapped by memo will be different. You can learn more about memo specifically. Here is an article recommended for everyone"From the source code Learn API Series React.memo》.

So for props.children, what you get every time is a new object returned byReact.createElement(About), which is also the reason why the props of the Home component have changed; and we use useRef, An object that will not change is created and assigned to the props of the Home component, so if the props of the Home component do not change, it will not be re-rendered.

solution

To solve this problem, in addition to using useRef, we can also define a variable. In addition to the App component, this effect can also be achieved, as shown below:

import {
    
     useState } from "react";
import Home from "./pages/Home";
import About from "./pages/About";

// 在组件外定义变量
const about = <About />;

function App() {
    
    
  const [count, setCount] = useState(0);
  console.log("App is render");

  return (
    <div className="App">
      {
    
    count}
      <button onClick={
    
    () => setCount(count + 1)}>Increment</button>
      <Home>{
    
    about}</Home>
    </div>
  );
}

When the About component does not depend on other states in the App component, we can use the above approach. However, if the About component also depends on other states in the App, you can find that neither the method of raising variables nor useRef can be implemented, such as in the About component. Receives a name parameter, passed in by the App component:

import React, {
    
     memo } from "react";

// 接收一个props.name
const About = memo(({
     
      name }) => {
    
    
  console.log("About is render");
  return <div>About: {
    
    name}</div>;
});

export default About;

At this time, we need to use useMemo for optimization (the reason for not using useCallback is that useCallback acts on the function and useMemo acts on the return value. Here it is obvious that we want to act on the component returned by the function), so that when the count changes , only the App component is re-rendered, and when the name attribute changes, App, Home, and About will all be re-rendered:

function App() {
    
    
  const [count, setCount] = useState(0);
  // 传入About组件的状态
  const [name, setName] = useState("Hello");
	// 使用useMemo优化
  const about = useMemo(() => <About name={
    
    name} />, [name]);

  console.log("App is render");

  return (
    <div className="App">
      {
    
    count}
      <button onClick={
    
    () => setCount(count + 1)}>Increment</button>
      <button onClick={
    
    () => setName("abc")}>Change Name</button>
      <Home>{
    
    about}</Home>
    </div>
  );
}

Guess you like

Origin blog.csdn.net/m0_71485750/article/details/134874037