facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
229.85k stars 47.06k forks source link

React 18 - Using SSR with bundle splitting is possible with `renderToString` #23237

Open wintercounter opened 2 years ago

wintercounter commented 2 years ago

I'm using Apollo Client with SSR, streaming is not an option for me, but bundle-splitting was always a pain point. I decided to investigate a bit. After a few hacks/adjustments, I'm having a fully working SSR site of React 18, Lazy components + Bundle splitting, and Apollo Client.

Docs/publications are stating that it's not possible because during render it'll immediately return upon finding a Promise. This is what was called limited Suspense support. However, in my case, I only need Suspense for Lazy.

I started to experiment with React 18 + Apollo's getDataFromTree (what I used before with 17 until now). I had to switch from renderToStaticMarkup to renderToString to preserve the special markers React is using during hydration. This wasn't a problem, Apollo supports passing a custom renderFunction which allowed me to replace.

Testing this simple setup with lazy components led to some interesting results. SSR was working, but on each refresh always new parts started to appear in the markup. After a few refresh the app reached it's final state and it was always correct from that point. I suspected first that Node's cached modules are the reason, tried to require all lazy modules on server side to pre-cache them, it didn't work.

Then I checked the source code of renderToString where I saw that lazy is actually caching the resolved values for later use.

I simply patched the created lazy modules to make it look like it's initialized, and voila, my application became sync on server side, everything works.

mod._payload._status = 1
mod._payload._result = require('pathToModule')

With all this, I was wondering if there's room later to provide an API for this. It's not a big deal IMO, and it'd be incredibly helpful to all people relying on renderToString.

For example something like lazy(() => import('./Search'), typeof global !== 'undefined'), where the second params is evalute. I'd even make it evaluated by default on server side, because why not?! :)

gaearon commented 2 years ago

Is there any particular reason you’d rather not migrate to the streaming renderToPipeableStream method? That one would emit all HTML (whether it’s lazy on the server too or not), and is generally recommended in 18 over renderToString.

wintercounter commented 2 years ago

Many libs do not support Suspense for data loading yet, like Apollo as well. For example Apollo's SSR currently works by doing complete rerenders while there are unresolved queries, then once it's done, I get all the data from the client, and manually add to the source using window.APOLLO_STATE. I don't see how this method can work with streams/pipe, maybe by using a custom stream first instead of Express's and then send it once it's ready, but that would be the same as renderToString at the end, doesn't it? :)

Anyway, it's not up to me to refactor these, I also don't really need streams anyway, I'm using a Redis cache for the entire response by URL as most of the content is static.