<Suspense> as a root element causes two instances of component in production and dev (no <StrictMode>)

Viewed 185

NOTE: this happens in both dev and production, and I'm not using <StrictMode> at all.

It appears to work fine:

B.js

export default function B()
{
    return <p>B</p>
}

TestApp.js

import { lazy, Suspense } from "react";
const B = lazy(()=>import("./B"));

export default function TestApp()
{
    const counter = useRef(0);
    counter.current++;
    console.log("rendering TestApp - counter",counter.current);
    return <Suspense fallback={<p>loading B...</p>}><B/></Suspense>
}

index.js

import { lazy, Suspense } from "react";
import {createRoot} from "react-dom/client";
const TestApp = lazy(()=>import("./TestApp"));

const root = createRoot(document.getElementById("root"));
root.render(<Suspense fallback={<p>loading testapp...</p>}><TestApp/></Suspense>);

but when there is an additional <Suspense> deeper in the component tree (in TestApp itself, suspending <B>), <TestApp> gets duplicated.

project: https://github.com/jmlee2k/react-suspense-root

demo: https://jmlee2k.github.io/react-suspense-root/ (production build)

To see the issue, go to the demo and open the console, you'll see "rendering TestApp - counter 1" twice. If this was simply a double-render, I would expect the counter to increase.

I'm fairly new to react and am very aware I could be doing something wrong, but any info would be appreciated.

Thanks in advance!

2 Answers

Your application has no unnecessary re-renders; it is how React and closures work.

As explained, the app first loads the suspense component and, once the lazy loading finishes, renders the TestApp component.

In that process, React mounts the TestApp component calling that function. There you have the first invoke count value. When B gets lazily loaded, the function is called again. But there is no re-render, since its state didn't change. All the variables inside the TestApp component got refreshed, except for the useRef one. That is why it is a good idea to have as few handlers inside the component (which gets refreshed on every render) and why it is a good idea to wrap code (aka effects) inside useEffect.

This has something to do with the way react does things. Whenever any component's state or properties are changed, react updates its virtual DOM tree. For example, here you can see how the component is rendered again when there is a change in state

const {useState} = React;

const Example = () => {
console.log("Hi mom!I re-rendered")
    const [count, setCount] = useState(0);
  
    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
};

// Render it
ReactDOM.createRoot(
    document.getElementById("root")
).render(
    <Example />
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
<div id="root"></div>

Keeping that in mind the flow of your application is the following :

  1. react loads TestApp and the suspense shows the fallback as component B hasn't loaded yet (first render)

  2. Component B finally loads and the suspense displays its content. (second rendering)

so there is nothing wrong with your code because it is the natural behavior of react, however, if there are many unnecessary re-renders it is possible that the application suffers from performance problems.

[Update]

I was looking around and apparently useRef isn't working but I tried console.count and it works now

import { lazy, Suspense, useRef } from "react";
const B = lazy(()=>import("./B"));

export default function TestApp()
{
    console.count('counter')

    return <Suspense fallback={<p>loading B...</p>}><B/></Suspense>
}
Related