theKashey / react-imported-component

✂️📦Bundler-independent solution for SSR-friendly code-splitting
MIT License
664 stars 39 forks source link

Flicker on hydrate when using Parcel SSR #201

Open thomasjm opened 4 years ago

thomasjm commented 4 years ago

I'm getting a white flicker that happens when ReactDOM.hydrate is called. I've tried my best to follow the documentation but I can't figure out how to fix it.

I trimmed down my project to a simple repro which you can find here: https://github.com/thomasjm/ric-hydrate-flicker

Any help would be much appreciated :)

theKashey commented 4 years ago

👍 will take a look

theKashey commented 4 years ago
  1. you have a mistake in SSR code - double closed div

    - fullHtml = fullHtml.replace(`<div id="app">`, `\n<div id="app">${html}</div>`)
    + fullHtml = fullHtml.replace(`<div id="app">`, `\n<div id="app">${html}`)
  2. you have a mistake in client code - dynamically created component. It is just unique every render (that is the root cause)

    
    + const Simple = lazy(() => import("./pages/simple"));
    ....
    - {renderPage(lazy(() => import("./pages/simple")))()}
    + {renderPage(Simple)()}
  3. Please use dev mode to debug the build - hydrate will complain about markup mismatch

    "create-bundle:client": "cross-env NODE_ENV=development BABEL_ENV=client parcel build app/index.html -d dist/client --no-source-maps --no-minify --public-url /dist/client",
  4. You can also check how its working using manual debugging

    console.log('before', element.innerHTML);
    ReactDOM.render(app, element);
    console.log('after', element.innerHTML);
thomasjm commented 4 years ago

Thanks!!! So happy to find a fix.

One question though -- I made those changes (just pushed them) and I'm still seeing a warning:

Warning: Did not expect server HTML to contain a <h2> in <div>.

However, the before and after prints both show <h2>Simple simple simple</h2>, so it seems like nothing changed. Shouldn't this warning be gone now?

(Note: your point number 4 used ReactDOM.render but I assume you mean ReactDOM.hydrate so I used that.)

theKashey commented 4 years ago

Well, look like using Suspense (LazyBoundary) is the root cause for this.

I've downgraded your example back to 16.9.0, and the problem was resolved once again. So here is the root cause - https://github.com/facebook/react/issues/16938 - which was expected to be resolved a while ago, but look like it strikes back.

According to this test - https://github.com/facebook/react/pull/16945/files#diff-ab371863932cd2e8f0ba14ff2eaab380R687 all you have to do (and it will fix the problem, I've tested) - remove fallback prop from LazyBoundary (typescript will complain).

theKashey commented 4 years ago

Citing https://github.com/facebook/react/pull/16945

So technically a workaround that works is actually setting the fallback to undefined on the server and during hydration. Then flip it on only after hydration.

Not very happy to discover this problem a year after it occurred (still on 16.9 on projects using Lazy). Tested 16.10.1 - and it's completely broken (eating dom nodes, not just recreating them). 😭

theKashey commented 4 years ago

Just checked:

thomasjm commented 4 years ago

Hmm, I tried switching to importedComponent and got it to hydrate without any warnings. I just pushed those commits.

Is there any reason to prefer the lazy API over the pre-lazy API? Is pre-lazy older or worse somehow? If importedComponent works properly then I'd rather use that than try to work around issues with LazyBoundary. But wondering what solution you'd recommend.

theKashey commented 4 years ago

Lazy gives you a Suspense boundary, so you can "await" loading on a bit higher level, including using new useTransition hooks to properly await for loading before actually changing the page.

It all depends on the use case, but if you have more that one deferred component on the page - you might prefer lazy