preactjs / preact

⚛️ Fast 3kB React alternative with the same modern API. Components & Virtual DOM.
https://preactjs.com
MIT License
36.66k stars 1.95k forks source link

Suspense rendering problems with Next.js #3501

Open filipjakov opened 2 years ago

filipjakov commented 2 years ago

Using Next.js (SSR) -> package.json:

"next": "12.1.2",
"next-plugin-preact": "3.0.6",
"preact": "10.7.0",
"preact-render-to-string": "5.1.20",
"react": "npm:@preact/compat@17.0.3",
"react-dom": "npm:@preact/compat@17.0.3",

Describe the bug

Note the different html elements, styles applied on them and how they get rendered. The Content component just does a SWR api call with 2s sleep so suspense can get triggered.

isClient is used to use suspense once on the client since (afaik) Suspense does not work on the server.

  1. Same html element:

Let's say I have the following content on the page:

{isClient() ? (
    <Suspense fallback={<h1 style={{ color: 'red' }}>Fallback</h1>}>
        <Content>
            <h1 style={{ color: 'blue' }}>Content</h1>
        </Content>
    </Suspense>
) : (
    <h1 style={{ color: 'green' }}>Else</h1>
)}
  1. Different html elements:
{isClient() ? (
    <Suspense fallback={<h1 style={{ color: 'red' }}>Fallback</h1>}>
        <Content>
            <h2 style={{ color: 'blue' }}>Content</h2>
        </Content>
    </Suspense>
) : (
    <h3 style={{ color: 'green' }}>Else</h3>
)}

Bugs

Expected behavior Suspense should work as expected: should render the fallback & should re-render(?) the consuming component once the suspended request is finished.

alex289 commented 2 years ago

I have some issues with NextJs too. When I try to build I get the error: Can't find preact/compat/client Can you successfully build your project?

filipjakov commented 2 years ago

@Alex289 yes, no build problems on my side

marvinhagemeister commented 2 years ago

@Alex289 can you open a new issue for that? It seems like the error you're seeing is not related to the issue here.

JoviDeCroock commented 2 years ago

I think this issue presents itself because we stay in hydration mode where we don't patch missmatches in DOM-attributes as this is commonly seen as a bug between Server and Client.

So DOM arrives from server as

<h3 style={{ color: 'green' }}>Else</h3>

then we switch to hydrate() and enter the isClient() branch, this suspends which makes us get

<h1 style={{ color: 'red' }}>Fallback</h1>

We hydrate but don't patch the innerText nor the style, nor the nodeType. When the Suspense resolves we see that the DOM still remains the same on

<h2 style={{ color: 'blue' }}>Content</h2>

We yet again haven't patched any of the needed things, I think this is because of our deferred hydration tricks we use when we are revolving around Suspending boundaries because we stay in hydration mode even though we will have to patch the nodeType, ... ideally

WDYT @marvinhagemeister @developit

developit commented 2 years ago

Is this type of client/server variance actually valid? React is likely destroying and recreating the tree here, which doesn't seem like a good point of reference for correctness. We could flip hydration-deferred subtrees into mutative hydration mode, but that's a performance drawback.

JoviDeCroock commented 2 years ago

Well I guess that's a valid point when SSR returns vastly different content but if we never escape deferred hydration mode and switch route this could occur as well right? i.e. different lazy route can result in the content getting hydrated and being a different Component all together

JoviDeCroock commented 2 years ago

Coming back to this @developit I do think React warns for this and that Lighthouse gives you CLS for this because we are changing the SSR response.

I think what they advice in React is to do