I am doing SSR with Reacts renderToPipeableStream
and use ApolloClient to fetch data in components. In order to rehydrate the serverside cache in the client i need to extract the cache and expose it on the global window object using an inline script in the html sent by the server.
Thankfully React 18 handles Suspense
during SSR and I can simply suspend rendering the script tag which exposes the cache until all requests are settled.
export const ApolloSSRCache = () => {
Object.values(renderCache).forEach((wrapped) => wrapped());
return (
<script
dangerouslySetInnerHTML={{
__html: `window.__APOLLO_STATE__=${JSON.stringify(
client.extract()
).replace(/</g, '\\u003c')};`,
}}
/>
);
};
The requests are wrapped in a suspender which throws the promise if it's not settled.
export function wrapPromise<T>(promise: Promise<T>): () => T {
let status = 'pending';
let response: T;
const suspender = promise.then(
(res) => {
status = 'success';
response = res;
},
(err) => {
status = 'error';
response = err;
}
);
return () => {
switch (status) {
case 'pending':
throw suspender;
case 'error':
throw response;
default:
return response;
}
};
}
This is working and all the requests made in any component during serverside rendering gets rehydrated into the client side cache which means the useQuery
hook on the client can use the cache to immediately serve a response. This is neccessary otherwise you make twice the number of requests and may cause flickering if you show a loading state when fetching data on the client.
At the moment I have to keep track of requests / promises manually and wait until all of them are settled. It seems a bit hacky to keep track of all requests made by Apollo if Apollo already has a cache in place that knows about the state of every request.
It needs to work with Suspense
because I use code splitting with dynamic imports..
Update:
If you use useSuspenseQuery
on the server side and put the script tag exposing your cache below the rendered children the cache will get populated fine because the components executing the query suspend.
Thus there's no need to manually suspend the tag as client.extract()
will be called after all requests settled.
Update 2:
useSuspenseQuery
ignores {fetchPolicy: 'network-only'}
and serves cached data during serverside rendering until the server is restarted.
There is no official way in React to do this at this point in time.
If you want to know more about the background and the "missing features in React", I have a conference talk on that: https://portal.gitnation.org/contents/the-rocky-journey-of-data-fetching-libraries-in-reacts-new-streaming-ssr
That said, it works quite well (with some hacks) in Next.js using the @apollo/experimental-nextjs-app-support
package, and we're right now in the process of creating a new @apollo/client-react-streaming
package that will also allow using that in other environments.
We'll probably release that within the next few weeks.
I'd recommend you to follow this PR that contains an integration test for Vite + renderToReadableStream
(and the other 0.9.0 PRs) - renderToPipeableStream
will be used slightly differently, you can see that with a version of React that has been extended with some "hacked hooks" in this integration test PR.
All that said: we'll not support useQuery
in streaming SSR. Apollo Client now ships with suspense-enabled hooks like useSuspenseQuery
, so we focus on those instead. useQuery
will only render once, and as a result will always render a loading state on the server - and you'd end up with a rehydration mismatch in the browser as a result if you tried to transport the data over for that.