react-imported-component icon indicating copy to clipboard operation
react-imported-component copied to clipboard

Flicker on hydrate when using Parcel SSR

Open thomasjm opened this issue 5 years ago • 8 comments

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 :)

thomasjm avatar Sep 09 '20 10:09 thomasjm

👍 will take a look

theKashey avatar Sep 09 '20 23:09 theKashey

  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}`)
  1. 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
```js
console.log('before', element.innerHTML);
ReactDOM.render(app, element);
console.log('after', element.innerHTML);

theKashey avatar Sep 10 '20 01:09 theKashey

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.)

thomasjm avatar Sep 10 '20 03:09 thomasjm

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

  • if you change lazy to importer - everything will work as before
  • if you will after that remove LazyBoundary, which is no longer required - the problem will be solved. 👉 So the problem is bound to Suspense boundary.

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 avatar Sep 10 '20 10:09 theKashey

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 avatar Sep 10 '20 10:09 theKashey

Just checked:

  • you can render all boundaries with fallback={undefined}
  • and update fallback to something defined in useLayoutEffect (for example)
  • however is any lazy would throw (for example components which were not server side rendered) that would throw an error. So it all depends on how "full" page is rendered, and if some pieces were not rendered - why and how.

theKashey avatar Sep 10 '20 11:09 theKashey

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.

thomasjm avatar Sep 12 '20 00:09 thomasjm

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

theKashey avatar Sep 13 '20 22:09 theKashey