remix-run / remix

Build Better Websites. Create modern, resilient user experiences with web fundamentals.
https://remix.run
MIT License
29.56k stars 2.49k forks source link

Hydration error on lazy imported component #9719

Closed WillSmithTE closed 2 months ago

WillSmithTE commented 3 months ago

Reproduction

Github

Deployed

I tried to create a stackblitz but failed, maybe needs node 20? Dunno

Create a new project with the official cloudflare starter (npm create cloudflare@latest my-remix-app -- --framework=remix).

Add a lazy-loaded component

System Info

System:
    OS: macOS 14.5
    CPU: (11) arm64 Apple M3 Pro
    Memory: 43.00 MB / 18.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.11.0 - ~/.nvm/versions/node/v20.11.0/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 10.2.4 - ~/.nvm/versions/node/v20.11.0/bin/npm
    pnpm: 9.1.4 - ~/Library/pnpm/pnpm
    Watchman: 2023.12.04.00 - /usr/local/bin/watchman
  Browsers:
    Brave Browser: 121.1.62.153
    Chrome: 126.0.6478.127
    Safari: 17.5
  npmPackages:
    @remix-run/cloudflare: ^2.9.2 => 2.10.2 
    @remix-run/cloudflare-pages: ^2.9.2 => 2.10.2 
    @remix-run/dev: ^2.9.2 => 2.10.2 
    @remix-run/react: ^2.9.2 => 2.10.2 
    vite: ^5.1.0 => 5.3.3 


### Used Package Manager

npm

### Expected Behavior

Upon loading or refreshing the page, no errors occur

### Actual Behavior

A hydration error occurs

`Error: Hydration failed because the initial UI does not match what was rendered on the server.`

Swapping the lazy loading for the standard default export removes the hydration error
brophdawg11 commented 3 months ago

I think you just need to add a Suspense boundary around the lazy component.

Server-side, the import can be resolved synchronously but then during hydration client-side, the module needs to be downloaded for the lazy component so React can't hydrate the with same UI the server-rendered and needs to suspend.

In my local testing, adding a suspense boundary resolves the hydration issue. Can you confirm this fixes your setup as well?

WillSmithTE commented 3 months ago

True, that will do, thanks!

I wanted to just "wait" to render while importing, but now you put it like that, I guess that doesn't make sense in the ssr -> hydration -> client render lifecycle, right?

brophdawg11 commented 2 months ago

yeah not really - generally you either (1) render the component during SSR and include it as a critical path import so it can hydrate, or (2) use suspense to render a spinner during SSR and then you can hydrate the spinner while the client fetches the lazy component.

If you really wanted to render the component during SSR and then delay hydration until the client had downloaded the component, you'd have to load the component ahead of hydrating your app in entry.client - but I wouldn't recommend this since the app will be unresponsive during that time

import('./lazy').then(() => {
  hydrateRoot(
    document,
    <StrictMode>
      <RemixBrowser />
    </StrictMode>
  );
});
WillSmithTE commented 2 months ago

makes sense, thanks. the app used to be a spa with a bunch of heavy components/imports for only certain users, so lazy let me have no CLS. i'll have to work out which way the ux should go now that it could be a SPA render or SSR