React 18 SSR Support
See original GitHub issueI’d like to start a discussion about React 18 and its changes to Suspense/SSR and what that means to us Apollo users. It was always a pain point for me that I couldn’t do proper bundle splitting (w/o 3rd party tools) due to the lack of Suspense support during SSR.
With React 18 we get SSR Suspense support, and it seems like the new pipeToNodeWritable
API can completely eliminate getDataFromTree
(at least in its current form).
The new API will be something like this. (https://codesandbox.io/s/github/facebook/react/tree/master/fixtures/ssr2?file=/server/render.js)
const {startWriting, abort} = pipeToNodeWritable(
<DataProvider data={data}>
<App assets={assets} />
</DataProvider>,
res,
{
onReadyToStream() {
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
res.write('<!DOCTYPE html>');
startWriting();
},
}
);
This has an interesting problem: where and how we will pass INITIAL_APOLLO_STATE
on. The previous common practice was to simply get the data from the cache, put it on window
in the markup, load it on the client-side during hydration.
For me, it looks like that we will be able to do this without any extra code/workaround now, and we can simply include the above process in our code. Something like:
<html>
<body>
...
<script dangerouslySetInnerHTML={{ __html: `
window.INITIAL_APOLLO_STATE = ${myInMemoryCacheInstance.extract()}
` }} />
</body>
</html>
I haven’t experimented with it yet, but I’ll soon and get back with the results.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:13
- Comments:10 (2 by maintainers)
Top GitHub Comments
@meierchris
Meanwhile, I have quickly put together a demo: https://github.com/wintercounter/react18-apollo-ssr-bundle-split-demo You can run it using GitPod or locally. I added some basic readme about the setup. Please let me know how it went.
Since I posted this solution here, I started to use it in production on a large codebase, I don’t have any issues.
Last week I decided to give another try to the topic, so I dug deep into the topic. I’ve made work Apollo SSR with React 18 and Suspense for lazy loading together, and actually, it’s not hard at all. I decided to share my method.
getDataFromTree
function. Apollo usesrenderToStaticMarkup
by default. The problem is that it will strip out all the necessary markers for hydration (in case you just need this for SEO reasons, just skip this step, you won’t hydrate in that case).hydrateRoot
method on client side.There is one gotcha, however.
On the first render, no data/tree will be returned. Once a lazy module was loaded, it’ll start working fine. So this means after page refresh it’ll work correctly. To overcome this issue I simply created a separate file on server-side that will load all the available lazy modules upon initialization.
In case you have
lazy(() => import('./components/moduleA'))
somewhere in you codebase. You must haverequire('./components/moduleA')
somewhere before SSR happens so the module is already in cache.I don’t know if this is a limitation of React’s SSR, Apollo’s
getMarkupFromTree
or it’s the result of swc’s transpilation to CommonJS, this should be investigated further.Seems like returning an immediately resolved promise for the module keeps rendering “sync” somehow.
Anyway, I have a working bundle splitting + Apollo + SSR combo finally!
UPDATE:
I was excited so I started to migrate a larger codebase. Turns out the above is just half of this module caching story. Seems like I need to have as many reloads as many lazy components I use in the codebase. I’ll get the correct markup and preloaded state only after that. It’s pretty interesting, I wonder what is happening exactly here.
UPDATE 2:
Turns out React is caching the resolved lazy values for later use. See here: https://github.com/facebook/react/blob/a724a3b578dce77d427bef313102a4d0e978d9b4/packages/react-dom/src/server/ReactPartialRenderer.js#L1296
I checked the values, and it’s simply can be mocked. I’m going to try it tomorrow and come back with the results.
UPDATE 3: I couldn’t wait, I tried now 😃 Works perfectly fine.
I created a
preCache.ts
file that I load at the initialization stage of the server. These modules are only having lazy exports.This is what the lazy exports look like. I just wanted to keep the import names at the same place, but it’s up to you how you patch your module later.
UPDATE 4: Found one more issue. All lazy components need to be wrapped with Suspense, otherwise,
renderToString
won’t return the required markers in the markup and hydration will fail.I create my own
lazy
function that solves this, so far so good.