简体   繁体   中英

How to rewrite this simple component tree to take advantage of React.memo?

In the followingCode Sandbox we have a <Child /> component that either renders within a <div /> or not, depending on state.

import React, { useState, memo } from "react";

const Child = memo(
  ({ n }) => {
    console.log("Re-rendered child");
    return <span>Child {n}</span>;
  },
  () => true
);

export default function App() {
  const [shouldWrapChildComponent, setShouldWrapChildComponent] = useState(
    false
  );

  return (
    <div>
      <button
        onClick={() =>
          setShouldWrapChildComponent(
            (shouldWrapChildComponent) => !shouldWrapChildComponent
          )
        }
      >
        Toggle Wrapper
      </button>

      <br />

      {[0, 1, 2, 3, 4, 5].map((n) => {
        return shouldWrapChildComponent ? (
          <div>
            <Child n={n} />
          </div>
        ) : (
          <Child n={n} />
        );
      })}
    </div>
  );
}

As you can see, the <Child /> component is using React.memo to prevent re-renders.

However, re-renders are not prevented when the component tree changes (and this makes sense). If shouldWrapChildComponent is true , then <Child /> will render inside of a <div /> , and otherwise <Child /> will render where <div /> used to render. This is what I mean by "the component tree changes".

Is there a way to rewrite the components such that the component tree does not change , and therefore can take advantage of React.memo? Or, given that we'll always need conditionally wrap the <Child /> component, is there no way to have React preserve the rendered output of <Child /> ?

However, re-renders are not prevented when the component tree changes (and this makes sense). If shouldWrapChildComponent is true, then will render inside of a , and otherwise will render where used to render. This is what I mean by "the component tree changes"

Let's talk about the above quotation, let's do a small change and remove the div element:

{[0, 1, 2, 3, 4, 5].map((n) => {
        return shouldWrapChildComponent ? (
          <Child n={n} />
        ) : (
          <Child n={n} />
        );
      })
}

The result is: your Child component will re-render only 6 times even after toggling with the button. You can put a console.log() on the App component and check the console, the toggle button will cause a re-render for your whole App but the Child component will not re-render anymore.

So, the memorization is working properly and prevents the child component from extra re-render.

But the problem appeared when you wrap your Child component with another element ( here with div ), why?

To check this behavior, I separate the main part from the return method by creating a resultArray variable just to log the resultArray before rendering.

import React, { useState, memo } from "react";

const Child = memo(
  ({ n }) => {
    console.log("Re-rendered child");
    return <span>Child {n}</span>;
  },
  () => true
);

export default function App() {
  const [shouldWrapChildComponent, setShouldWrapChildComponent] = useState(
    false
  );

  const resultArray = [0, 1, 2, 3, 4, 5].map((n) => {
        return shouldWrapChildComponent ? (
          <div>
            <Child n={n} />
          </div>
        ) : (
          <Child n={n} />
        );
      })

  console.log(`When shouldWrapChildComponent is ${shouldWrapChildComponent} the result array is: `, resultArray);

  return (
    <div>
      <button
        onClick={() =>
          setShouldWrapChildComponent(
            (shouldWrapChildComponent) => !shouldWrapChildComponent
          )
        }
      >
        Toggle Wrapper
      </button>
      <br />
      {resultArray}
    </div>
  );
}

The resultArray when shouldWrapChildComponent is false :

>(6) [Object, Object, Object, Object, Object, Object]

>0: Object
  type: "div"  // pay attention here
  key: null
  ref: null
  >props: Object
     n: 0         // ----> passing n via props to Child
  _owner: FiberNode
  _store: Object
>1: Object
>2: Object
>3: Object
>4: Object
>5: Object

The resultArray when shouldWrapChildComponent is true :

>(6) [Object, Object, Object, Object, Object, Object]

>0: Object
  type: null    // pay attention here
  key: null
  ref: null
  >props: Object     // -----> passing a object instead of Child
    >children: Object
      >type: Object
        >type:   ƒ _c() {}
        >compare: f () {}
  _owner: FiberNode
  _store: Object
>1: Object
>2: Object
>3: Object
>4: Object
>5: Object

As you can see, the result is totally different, so react will trigger a re-render every time to draw the new elements in DOM.

React Reconciliation Algorithm

When diffing two trees, React first compares the two root elements. The behavior is different depending on the types of the root elements.

So, as the element type gets changed, the Reconciliation algorithm flagged it and tried to re-rendered it, this re-render is like the first render on your component before the component did mount (and cause extra re-render with your Child element).

More on React official documentation .

As a simplest solution

There are many solutions to avoid this extra re-rendering, but as a simple solution you can change the main part as below:

{[0, 1, 2, 3, 4, 5].map((n) => {
        return shouldWrapChildComponent ? (
          <div>
            <Child n={n} />
          </div>
        ) : (
          <div style={{display: "inline"}}>
            <Child n={n} />
          </div>
        );
      })
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM