Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Discussion] using Apollo Client's queryPreloader in TanStack router loaders #1675

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

phryneas
Copy link

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 loader function return values
  • Hook into deserialization of loader function values as soon as possible
    • Best would be directly after the loader data made it over the wire, but at the latest when useLoaderData is called
    • This deserialization needs access to the current Apollo Client instance and deserialzation triggers a side effect
    • This could be done by closing over client in createRouter, but it would be even better if the transformer had access to the current context in some way - this way, router.update could change the client instance without things breaking.

So, this is much less of a feature request than I initially imagined - more of a bunch of questions at this point:

  • Can transformer return objects that contain Promise intances?
  • 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 context to the transformer?
  • 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 :)

Copy link

vercel bot commented May 28, 2024

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

@lithdew
Copy link
Contributor

lithdew commented May 28, 2024

Things I believe to be true from reading the source code:

  1. AFAIK transformer can 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).
  2. Same goes for streams.
  3. Happens inside of <Scripts/> which has <DehydrateRouter/> nested inside of it which calls the provided transformer. Basically while rendering the client.
  4. If Apollo has its own transformer for hydration/dehydration, the router provides hydrate and dehydrate options. You have access to the context while constructing the router.
  5. 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:

  1. To wrap the router tree with context providers, the router provides InnerWrap and Wrap options.

@phryneas
Copy link
Author

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)

@lithdew
Copy link
Contributor

lithdew commented May 28, 2024

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?

@phryneas
Copy link
Author

phryneas commented May 28, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants