0

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

Kirshach
  • 23
  • 5
  • When you do a bunch of side-effects in the component body (the ref count, console logging) you're going to have odd results. The `useMemo` hook is for memoizing values, not components, use the `memo` Higher Order Component for that. – Drew Reese Apr 09 '21 at 09:30
  • But isn't the return of a functional component call just another value? I know that code is not idiomatic, but I can't see why exactly it doesn't work. Also, in this case using React.memo() does not change the behaviour, so for me the question of what happens here remains open – Kirshach Apr 09 '21 at 09:36
  • Well, *everything* is *a value*. All of your code is very anti-pattern (*though you got the boolean toggling correct, so +1 for that*), and your question is basically "why doesn't my code work?" It's because you haven't really followed any established guidelines of React. If you want to memoize a React component, the [memo Higher Order Component](https://reactjs.org/docs/react-api.html#reactmemo) is the answer. The rest of your question about the console logs and ref mutations are unintentional side-effects because you aren't running *that* code in a lifecycle, i.e. *useEffect*. – Drew Reese Apr 09 '21 at 09:49
  • Well ok, thanks for responding, I just wanted to have an answer on why exactly is this code anti-pattern, and what happens under the hood that doesn't make it work. I'm not doubting it ain't good, but my question is why exactly is it. "Because it doesn't follow the guidelines" - these guidelines might be more of a consequence of some reasoning that I wish to understand. – Kirshach Apr 09 '21 at 10:01
  • And, as I said before, the memo HOC doesn't do the job here – Kirshach Apr 09 '21 at 10:02

2 Answers2

2

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.

  • 1
    You're amazing! Thank you so much, now I have to digest how this might work and in to which applications that could lead – Kirshach Apr 09 '21 at 11:00
1

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 to renders.current on every render even though if I put a console.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".

enter image description here

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

Demo

Edit why-doesnt-memoization-of-a-react-functional-component-call-with-usememo-hook

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Oh, well, thanks a lot for the second part of the aswer! – Kirshach Apr 09 '21 at 10:29
  • So, this was the code I wrote initially, except for not wrapping `render.current` increment and `console.log` in `useEffect`, and it appeared to me that components still get re-rendered on every mount, which makes sense. So I thought that theoretically I could memoize component calls with a `useMemo` hook, which would return an object that would stay the same between multiple `` renders, and that would fix it. Do you know why it didn't work? I'm just confused as to why `console.log`s appear to run every render. Could it be that the returned objects trigger re-render calls on mount? – Kirshach Apr 09 '21 at 10:47
  • Oh wow... actually adding a key prop to a memoized return value as Ashish Tuteja suggested does work! – Kirshach Apr 09 '21 at 10:53
  • @Kirshach Yeah, Ashish mentioned the reconciliation process bit about the order changing and this causing the components to remount. Using a stable React key on those components indicates to React that they are still the same components from the previous render cycle. Side note, React `key` and `ref` aren't props in the conventional sense, they aren't passed on to children. – Drew Reese Apr 09 '21 at 15:15