admin管理员组

文章数量:1279148

I came across this answer, which appears to address this: When does React re-render child ponent?

But I'm asking a little more nuanced question here. Why does React re-render child ponents (generally) when using the useMemo hook?

In the below example, I'd expect both Child and Mchild ponents not re-render on an input onChange event, but only Mchild doesn't re-render. Child renders on every keypress.

Could anyone shed some light on why React does this? I suppose what I'm asking is, I don't understand why React doesn't do this by default. What would be the disadvantage of using a child ponent pattern that always uses React.memo?

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

const Child = ({ person, which }: any) => {
  console.log(`${which} render`);

  return <div>{`From child ponent: ${person.first}`}</div>;
};

const Mchild = React.memo(Child);

function Parent() {
  console.log("Parent render");

  const [message, setMessage] = useState<string>("");

  const person = { first: "gary", last: "johnson" };
  const mPerson = useMemo(() => person, []);

  return (
    <div>
      <div>
        MESSAGE:{" "}
        <input value={message} onChange={(e) => setMessage(e.target.value)} />
      </div>

      <div>Parent</div>

      <Child person={mPerson} which="Child" />
      <Mchild person={mPerson} which="Mchild" />
    </div>
  );
}

export default Parent;

I came across this answer, which appears to address this: When does React re-render child ponent?

But I'm asking a little more nuanced question here. Why does React re-render child ponents (generally) when using the useMemo hook?

In the below example, I'd expect both Child and Mchild ponents not re-render on an input onChange event, but only Mchild doesn't re-render. Child renders on every keypress.

Could anyone shed some light on why React does this? I suppose what I'm asking is, I don't understand why React doesn't do this by default. What would be the disadvantage of using a child ponent pattern that always uses React.memo?

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

const Child = ({ person, which }: any) => {
  console.log(`${which} render`);

  return <div>{`From child ponent: ${person.first}`}</div>;
};

const Mchild = React.memo(Child);

function Parent() {
  console.log("Parent render");

  const [message, setMessage] = useState<string>("");

  const person = { first: "gary", last: "johnson" };
  const mPerson = useMemo(() => person, []);

  return (
    <div>
      <div>
        MESSAGE:{" "}
        <input value={message} onChange={(e) => setMessage(e.target.value)} />
      </div>

      <div>Parent</div>

      <Child person={mPerson} which="Child" />
      <Mchild person={mPerson} which="Mchild" />
    </div>
  );
}

export default Parent;
Share Improve this question edited Jan 20, 2023 at 19:45 Gary asked Jan 17, 2023 at 2:32 GaryGary 1,0171 gold badge15 silver badges33 bronze badges 10
  • I posted an answer to explain things from the ground up. Please check my latest edit and let me know :) – Youssouf Oumar Commented Jan 17, 2023 at 17:42
  • Thank you Yousoumar. So to summarize your answer -- React doesn't use React.memo by default b/c it's more expensive than just allowing the child ponents to re-render, unless of course it isn't, at which point you'd use React.memo explicitly? – Gary Commented Jan 18, 2023 at 2:06
  • My pleasure @Gary :) I have added one last case that I did not mention before when your memorized ponent wraps another ponent. Check it and let me know! – Youssouf Oumar Commented Jan 18, 2023 at 10:12
  • How do you measure how expensive a re-render is vs. a React.memo invocation? – Gary Commented Jan 18, 2023 at 23:33
  • 1 I gave more thought to this and here's my conclusion -- we're evaluating O(number of shallow parisons [one per prop]) vs. a re-render of the child ponent; I'm fairly confident that a re-render of the child ponent is more pute intensive than a few shallow parisons so I'm still not sure why React.memo isn't the default. Maybe it's an issue of practicality -- that most apps wouldn't use the useMemo hook for props so they opted for full child re-renders every time. – Gary Commented Jan 21, 2023 at 3:49
 |  Show 5 more ments

2 Answers 2

Reset to default 10

A ponent re-renders when its internal state changes or its parent re-render. React doesn't memoize everything by default because, first, most re-renders are not expansive, and second, to be able to memoize, you need a parison algorithm, which is not free, as Dan Abramov, one of the maintainers says:

Shallow parisons aren’t free. They’re O(prop count). And they only buy something if it bails out. All parisons where we end up re-rendering are wasted.

Why would you expect always paring to be faster? Considering many ponents always get different props.

// Default rendering behavior overview 

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

function Parent() {
  const [state, setState] = React.useState(true);
  console.clear();
  console.log("Parent render");
  return (
    <div>
      <p>Default rendering behavior overview </p>
      <SimpleChild  />
      <button onClick={() => setState((prev) => !prev)}>Render Parent</button>
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

A ponent re-rendering because of its parent re-rendering may be problematic, if this child does expansive puting tasks, without being affected by the parent re-render. In this case, you can tell React not to re-render this child when the parent re-renders, with memo:

// Memoizing with memo

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

function Parent() {
  const [state, setState] = React.useState(true);
  console.clear();
  console.log("Parent render");
  return (
    <div>
      <p>Memoizing with memo</p>
      <HeavyChild  />
      <button onClick={() => setState((prev) => !prev)}>Render Parent</button>
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Now, what happens if we pass an object to the above memoized HeavyChild? Well, our memo won't do what we want anymore. This is because memo does "something like":

if (prevPropsValue === currentPropsValue) { 
    // Don not re-render
}

But an object defined in the parent gets re-created on a new reference on each re-render, so it's a new value.

// Memoizing with memo when an object is passed as props

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

function Parent() {
  const [state, setState] = React.useState(true);
  const someObject = {}
  console.clear();
  console.log("Parent render");
  return (
    <div>
      <p>Memoizing with memo when an object is passed as props</p>
      <HeavyChild someObject={someObject}  />
      <button onClick={() => setState((prev) => !prev)}>Render Parent</button>
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

If you wanna avoid the above behavior when it es to objects being passed as props, you can use useMemo, to memoize the object:

We have the same behavior as objects when it es to functions being passed as props, in which case we use useCallback to memoize them.

// Memoizing with memo when a memoized object is passed as props

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

function Parent() {
  const [state, setState] = React.useState(true);
  const someMemoizedObject = React.useMemo(()=>{}, [])
  console.clear();
  console.log("Parent render");
  return (
    <div>
      <p>Memoizing with memo when a memoized object is passed as props</p>
      <HeavyChild someMemoizedObject={someMemoizedObject}  />
      <button onClick={() => setState((prev) => !prev)}>Render Parent</button>
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

And the last thing to know is that memo won't work as expected if HeavyChild wrap a child ponent that can be consumed with children, as in this case, children is, as of now, always a new reference.

// Memoizing with memo when a ponent is passed as children

const SomeNestedChild = () => {
  return <div></div>;
};

const HeavyChild = React.memo(({children}) => {
  console.log("HeavyChild render");
  return <div></div>;
});

function Parent() {
  const [state, setState] = React.useState(true);
  
  console.clear();
  console.log("Parent render");
  
  return (
    <div>
      <p>Memoizing with memo when a ponent is passed as children</p>
      <HeavyChild><SomeNestedChild/></HeavyChild>
      <button onClick={() => setState((prev) => !prev)}>Render Parent</button>
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Let's see first when React ponents re-render. React children will re-render when either their props/state change or their parent ponent re-renders.

In your case, the Child will re-render since there is a state change in the parent ponent. Whenever you type in the input field, the state message changes, causing the parent ponent to re-render, hence the Child ponent will re-render.

const [message, setMessage] = useState<string>("");

In order to make your Child ponent only re-render when its prop changes, you need to memoize it, like you have done with MChild. Now MChild will only re-render when its prop changes, which is exactly the case of yours.

本文标签: