useMemo behaves different for inline component vs. normal component

Viewed 367

I have the following React code

const { useState, useMemo, Fragment } = React;

function Rand() {
    return <Fragment>{Math.random()}</Fragment>;
}

const App = () => {
    const [show, setShow] = useState(true);
    
    // The inline component gets memoized. But <Rand /> does not
    const working = useMemo(() => <Fragment>{Math.random()}</Fragment>, []);
    // The rand component is not memoized and gets rerendred
    const notWorking = useMemo(() => <Rand />, []);
  
    return(
        <Fragment>
            <button
                onClick={() => {
                  setShow(!show);
                }}>
                {show?"Hide":"Show"}
            </button>
            <br />
            Working: 
            {show && working}
            <br />
            Not Working: 
            {show && notWorking}
        </Fragment>
    );
}

ReactDOM.render(
    <App />,
    document.getElementById("root")
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

It uses useMemo 2 times.

The first time it uses an inline component to "initialize" and memoize a component ( const working = useMemo(() => <>{Math.random()}</>, []);)

The second time it uses a component which was made outside the app component (const notWorking = useMemo(() => <Rand />, []);)

Both components used in the useMemo function have the exact same code, which is <>{Math.random()}</>.

Here comes the unexpected part, when I hide (Click the button) and show the two memoized components again, they behave differently. The first one will always show the same random number which it got when it first got initialzied. While the seconds one will re-initialize each time.

First render

enter image description here

Second render (hide)

enter image description here

Third render (show again)

enter image description here

You can see from the screenshots that the first component's random number stays the same, while the second one does not.

My Questions:

  • How can I prevent in both cases to re-render/re-initialize the component?
  • Why does it currently behave as it does?

Interestingly, it does get memoized if I use a counter instead of show/hide:

const { useState, useMemo, Fragment } = React;

function Rand() {
    return <Fragment>{Math.random()}</Fragment>;
}

const App = () => {
    const [counter, setCounter] = useState(0);
    
    // The inline component gets memoized. But <Rand /> does not
    const working = useMemo(() => <Fragment>{Math.random()}</Fragment>, []);
    // The rand component is not memoized and gets rerendred
    const notWorking = useMemo(() => <Rand />, []);
  
    return(
        <Fragment>
            <button
                onClick={() => {
                  setCounter(c => c + 1);
                }}>
                Update ({counter})
            </button>
            <br />
            Working: 
            {working}
            <br />
            Not Working: 
            {notWorking}
            <br />
            <code>Rand</code> used directly:
            <Rand />
        </Fragment>
    );
}

ReactDOM.render(
    <App />,
    document.getElementById("root")
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

Here is a codepen to try it yourself https://codepen.io/brandiatmuhkuh/pen/eYWmyWz

1 Answers

Why does it currently behave as it does?

<Rand /> doesn't call your component function. It just calls React.createElement to create the React element (not an instance of it). Your component function is used to render an element instance, if and when you use it. In your "working" example you're doing:

<>{Math.random()}</>

...which calls Math.random and uses its result as text (not a component) within the fragment.

But your "not working" example just does:

<Rand />

The element is created, but not used, and your function isn't called. The "your function isn't called" part may be surprising — it was to me when I started using React — but it's true:

const { Fragment } = React;

function Rand() {
    console.log("Rand called");
    return <Fragment>{Math.random()}</Fragment>;
}

console.log("before");
const element = <Rand />;
console.log("after");

// Wait a moment before *using* it
setTimeout(() => {
    ReactDOM.render(
        element,
        document.getElementById("root")
    );
}, 1000);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

How can I prevent in both cases to re-render/re-initialize the component?

If you do what you've done in your example, which is to take the mounted component out of the tree entirely, you're unmounting the component instance; when you put it back, your function will get called again, so you'll get a new value. (This is also why the version with the counter doesn't exhibit this behavior: the component instance remained mounted.)

If you want to memoize what it shows, one approach is to pass that to it as a prop, and memoize what you pass it:

const { useState, useMemo, Fragment } = React;

function Rand({text}) {
    return <Fragment>{text}</Fragment>;
}

const App = () => {
    const [show, setShow] = useState(true);
    
    const working = useMemo(() => <Fragment>{Math.random()}</Fragment>, []);
    const randText = useMemo(() => String(Math.random()), []);

    return(
        <Fragment>
            <button
                onClick={() => {
                  setShow(!show);
                }}>
                {show?"Hide":"Show"}
            </button>
            <br />
            Working: 
            {show && working}
            <br />
            Also Working Now: 
            {show && <Rand text={randText} />}
        </Fragment>
    );
}

ReactDOM.render(
    <App />,
    document.getElementById("root")
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

Related