[Discussion] using Apollo Client's `queryPreloader` in TanStack router loaders
Please don't merge, this is just a PR because it allows me to easily highlight code changes to the default example :)
We talked about support for Apollo Client's queryPreloader in TanStack router loaders yesterday, and I promised you a write-up what we need.
Having created this example, I'm not even sure if we need anything more at this point 😅
Generally, we need these things:
- Hook into serialization of
loaderfunction return values - Hook into deserialization of
loaderfunction values as soon as possible- Best would be directly after the loader data made it over the wire, but at the latest when
useLoaderDatais called - This deserialization needs access to the current Apollo Client instance and deserialzation triggers a side effect
- This could be done by closing over
clientincreateRouter, but it would be even better if the transformer had access to the currentcontextin some way - this way,router.updatecould change the client instance without things breaking.
- Best would be directly after the loader data made it over the wire, but at the latest when
So, this is much less of a feature request than I initially imagined - more of a bunch of questions at this point:
- Can
transformerreturn objects that containPromiseintances? - What about streams? That would allow us to support GraphQL features like
@defer, where the response arrives in chunks, so this would be really cool! - When does deserialization happen? Immediately when data arrives?
- Do you see any chance of exposing
contextto thetransformer? - A bit of a quality-of-life thing: could you imagine to allow an array of transformers in
transformer? Could be useful if more libraries start shipping transformers :)
Adding @jerelmiller to the discussion, too :)
The latest updates on your projects. Learn more about Vercel for Git ↗︎
| Name | Status | Preview | Comments | Updated (UTC) |
|---|---|---|---|---|
| start-basic | ✅ Ready (Inspect) | Visit Preview | 💬 Add feedback | May 28, 2024 11:09am |
Things I believe to be true from reading the source code:
- AFAIK
transformercan return Promise/Stream objects should the transformer support it. It must be able to hydrate/dehydrate them. Examples of transformers that can do this is i.e. seroval. If the promises are suspended in a React component then React takes care of the serialization/deserialization - see the source code for defer() and useAwaited() where they throw a promise (but add some additional state to aid with keeping track of whether their state is pending/errored/success). - Same goes for streams.
- Happens inside of
<Scripts/>which has<DehydrateRouter/>nested inside of it which calls the providedtransformer. Basically while rendering the client. - If Apollo has its own transformer for hydration/dehydration, the router provides
hydrateanddehydrateoptions. You have access to the context while constructing the router. - The types for "piping" transformers isn't that nice so I don't think it would be implemented.
Also looking at the changes in the PR:
- To wrap the router tree with context providers, the router provides
InnerWrapandWrapoptions.
Thanks for the pointers!
Reading up on it, I believe I rather need transform than hydrate/dehydrate, since I only need to transport the QueryRef objects, no Apollo Client state beyond that, correct?
(QueryRefs need to register with the Apollo Client on deserialization, but they are not part of it)
I'm not too familiar with how Apollo Client handles hydration/dehydration, but from a quick cursory look at the docs:
Server-side:
// Add this import to the top of the file
import { getDataFromTree } from "@apollo/client/react/ssr";
// Replace the TODO with this
getDataFromTree(App).then((content) => {
// Extract the entirety of the Apollo Client cache's current state
const initialState = client.extract();
// Add both the page content and the cache state to a top-level component
const html = <Html content={content} state={initialState} />;
// Render the component to static markup and return it
res.status(200);
res.send(`<!doctype html>\n${ReactDOM.renderToStaticMarkup(html)}`);
res.end();
});
Client-side:
const client = new ApolloClient({
cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
uri: 'https://example.com/graphql'
});
If this method also dehydrates the client's cache to support streaming as well, you could call client.extract() in hydrate() { ... } and new InMemoryCache.restore(stringFromHydrate) in dehydrate() { ... }.
Otherwise, could you link to any docs or some source code on how to get hydration via. streaming working with Apollo Client?
Otherwise, could you link to any docs or some source code on how to get hydration via. streaming working with Apollo Client?
There is no extra hydration beyond what I showed here - hydration would be a side effect of the queryRef parsing, only hydrating that small part of the cache - it would all be hidden inside transform.parse.
These old patterns of "dehydrate the full store" and "hydrate the full store" don't work in renderToStream scenarios anymore, since the store is already created (and interactive) in the browser long before it will be filled on the server - so instead we have to selectively hydrate pieces as they make it over, in this case in the form of a QueryRef object.
We have alternative transport mechanisms for the suspenseful useSuspenseQuery/useBackgroundQuery/useReadQuery hooks, but these are quite complex to set up if not directly supported by a framework, so in a first step I'd try to concentrate on the loader scenario, before we get into that with TanStack Start :)
Oh, this can be closed regarding newer PRs.
Oh, this can be closed regarding newer PRs.
Can you clarify what you mean? Was there another PR addressing this?
@mirague I have all the changes I need in #2698 and after we get that merged, we can cat this released: https://github.com/apollographql/apollo-client-nextjs/blob/fd8d289791d62f3d644fa6cbb28f827dd31a370d/packages/tanstack-start/README.md