Reputation: 23
Okay, I think I might as well add here that I know that code doesn't look too good, but I thought it would do the job, so I would be glad if you could explain why not only it would not be a good practice if it worked, but why it doesn't work. I'd be glad to hear your solutions to the problem too!
I'm having a hard time understanding why the chunk of code below doesn't seem to work as intended by which I mean memoizing <First />
and <Second />
functional components' return values and not calling their functions on every <App />
render. I thought that since <SomeComponent />
expression returns an object, it would be possible to simply memoize it and go with. Doesn't seem to work and I can't wrap my head around of as to why.
On a side note, I would also be thankful if you could explain why rendering <App />
component causes the renders.current
value increment by two while only calling the console.log
once.
Thanks a lot for your help!
import "./styles.css";
import React from "react";
const First = () => {
const renders = React.useRef(0);
renders.current += 1;
console.log('<First /> renders: ', renders.current);
return <h1>First</h1>;
}
const Second = () => {
const renders = React.useRef(0);
renders.current += 1;
console.log('<Second /> renders: ', renders.current);
return <h1>Second</h1>;
}
const App = () => {
const [isSwapped, setIsSwapped] = React.useState(false);
const [, dummyState] = React.useState(false);
const first = React.useMemo(() => <First />, []);
const second = React.useMemo(() => <Second />, []);
const renders = React.useRef(0);
renders.current += 1;
console.log('<App /> renders: ', renders.current);
return (
<div className="App">
<button onClick={() => setIsSwapped((isSwapped) => !isSwapped)}>Swap</button>
<button onClick={() => dummyState((state) => !state)}>Re-render</button>
{isSwapped ? (
<>
{first}
{second}
</>
) : (
<>
{second}
{first}
</>
)}
</div>
);
}
Edit: Thanks for replies, guys, this is the version that does what was intended: https://codesandbox.io/s/why-doesnt-memoization-of-a-react-functional-component-call-with-usememo-hook-forked-gvnn0?file=/src/App.js
Upvotes: 0
Views: 1906
Reputation: 203457
I tried your code with the components' return values memoized via the useMemo
hook and then wrapped each with React.memo
and the result appeared to be the same for me.
Using the memo Higher Order Component to decorate a component will memoize the rendered result.
If your component renders the same result given the same props, you can wrap it in a call to
React.memo
for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.
const First = React.memo(() => {
const renders = React.useRef(0);
React.useEffect(() => {
renders.current += 1;
console.log("<First /> renders: ", renders.current);
});
return <h1>First</h1>;
});
const Second = React.memo(() => {
const renders = React.useRef(0);
React.useEffect(() => {
renders.current += 1;
console.log("<Second /> renders: ", renders.current);
});
return <h1>Second</h1>;
});
You'll see I've also addressed the unintentional side-effects (console log and ref mutation) in the following question:
On a side note, it would also be cool if you could help me with understanding why parent component seems to be adding
+2
torenders.current
on every render even though if I put aconsole.log('render')
in the component, it will only show up once.
This is because there are two phases of the component lifecycle, the "render phase" and the "commit phase".
Notice that the "render" method occurs during the "render phase" and recall that the entire component body of functional components is the "render method". This means that React can and will possible render the component several times in order to compute a diff for what needs to be rendered to the DOM during the "commit phase". The "commit phase" is what is traditionally considered rendering a component (to the DOM). Note also that this phase is where side-effect can be run, i.e. useEffect
.
Place the logs and ref update in an useEffect
hook to run them once per render cycle.
function App() {
const [isSwapped, setIsSwapped] = React.useState(false);
const [, dummyState] = React.useState(false);
const renders = React.useRef(0);
React.useEffect(() => {
renders.current += 1;
console.log("<App /> renders: ", renders.current);
});
return (
<div className="App">
<button onClick={() => setIsSwapped((isSwapped) => !isSwapped)}>
Swap
</button>
<button onClick={() => dummyState((state) => !state)}>Re-render</button>
{isSwapped ? (
<>
<First />
<Second />
</>
) : (
<>
<Second />
<First />
</>
)}
</div>
);
}
Upvotes: 1
Reputation: 168
For the second part of your question relating to why the value of render.current might be updating twice rather than once it might be because you are using React.StrictMode in your index.jsx file. Removing StrictMode did seem to fix the issue in this example. Also check out this link for more info.
As for the first issue, it seems that on clicking the re-render button neithr of the child components (First or Second) re-render. However, when we swap them not only do they re-render but the components are also unmounted and then re-mounted. You can see this behaviour in the above example, as well. Providing unique keys for both components seems to fix this issue. I'm also a beginner at React and not entirely sure of what's happening behind the scenes, however this is what I have gathered so far based on the documentation: When react finds that a child (say the first child) has a different type compared to what the type was on the previous render (in our case the type switches between First and Second) it unmounts the child node and all of its children, then re-mounts them, then continues with the rendering process. This is supported by the console logs we can see int the above example. By including unqiue keys we let react know that despite components First and Second being in different locations they are the same component after all. So, react doesn't bump into a scenario where a child has swapped types.
Upvotes: 2