Using toPromise in a tanstack router loader loses internal queryRef
Issue Description
When using a preload query from createQueryPreloader in a tanstack router loader method and .toPromise(), the resolved queryRef is lost when navigating between routes, but not on refresh.
For example, given this apollo setup:
import { ApolloClient, createQueryPreloader, InMemoryCache } from "@apollo/client";
export const apollo = new ApolloClient({
uri: "https://rickandmortyapi.com/graphql",
cache: new InMemoryCache(),
});
export const preloadQuery = createQueryPreloader(apollo);
And this route:
import { graphql } from '@/gql'
import { preloadQuery } from '@/utils/graphql'
import { useReadQuery } from '@apollo/client'
import { createFileRoute, useLoaderData } from '@tanstack/react-router'
const GetRickDocument = graphql(`
query GetRick {
characters(page: 1, filter: { name: "Rick" }) {
results {
id
name
}
}
}
`)
export const Route = createFileRoute('/rick')({
component: RouteComponent,
loader: async () => {
// If I don't use toPromise, it works as expected but the query doesn't block
// the loader.
//
// const queryRef = preloadQuery(GetRickDocument);
const queryRef = await preloadQuery(GetRickDocument).toPromise();
// Looks correct even when navigating pages
console.log('queryRef from loader', queryRef);
return queryRef;
}
})
function RouteComponent() {
const queryRef = useLoaderData({ from: Route.id })
// Missing internal refs when navigating pages (but not on refresh)
console.log('queryRef from component', queryRef)
const { data: { characters }} = useReadQuery(queryRef);
return (
<div>
<h1>Ricks</h1>
<ul>
{characters?.results?.map(character => (
<li key={character?.id}>{character?.name}</li>
))}
</ul>
</div>
)
}
When navigating directly to http://localhost:3000/rick, or refreshing the page, the page loads as expected. However, when navigating to the page via a link, I get the error:
Expected a QueryRef object, but got something else instead.
Logging out the queryRef from the loader shows the expected output:
queryRef from loader {Symbol(apollo.internal.queryRef): InternalQueryReference2, Symbol(apollo.internal.refPromise): Promise, toPromise: ƒ}
However the queryRef in the component seems to lose it's internal reference:
rick.tsx:38 queryRef from component {Symbol(apollo.internal.queryRef): InternalQueryReference2, Symbol(apollo.internal.refPromise): Promise, toPromise: ƒ}
rick.tsx:38 queryRef from component {Symbol(apollo.internal.queryRef): InternalQueryReference2, Symbol(apollo.internal.refPromise): Promise, toPromise: ƒ}
rick.tsx:38 queryRef from component {toPromise: ƒ}
rick.tsx:38 queryRef from component {toPromise: ƒ}
Link to Reproduction
https://github.com/jeremyoverman/apollo-loader-demo
Reproduction Steps
- Clone the reproduction repo
-
npm install -
npm run dev - Navigate to either
RickorMorty - Notice the
Expected a QueryRef object, but got something else instead.error - Check the console to see the incorrect value of
queryRef from component - Refresh the page, notice it loads as expected
@apollo/client version
3.13.8
Hey @jeremyoverman 👋
I'll need to play around with this some more, but I'd be curious what happens if you return queryRef in an object:
export const Route = createFileRoute('/rick')({
component: RouteComponent,
loader: async () => {
const queryRef = await preloadQuery(GetRickDocument).toPromise();
return { queryRef };
}
})
function RouteComponent() {
const { queryRef } = Route.useLoaderData({ from: Route.id })
// ...
}
I'm not super familiar with TanStack Router yet as I've mostly used React Router, but I wonder if its doing some sort of promise detection on object properties and this is causing that queryRef to break apart in your component (this is just my theory, but I don't have concrete evidence for this). Let me know if that changes anything!
Hey @jerelmiller 👋
Just gave that a shot and no luck, still the same issue. I believe Tanstack router does have some promise handling in the loader though, but I'm not entirely familiar with the inner workings there either 🤔
Just gave returning the promise itself a shot without awaiting it as well, but have the same issue.
export const Route = createFileRoute('/rick')({
component: RouteComponent,
loader: () => preloadQuery(GetRickDocument).toPromise(),
})
function RouteComponent() {
const queryRef = useLoaderData({ from: Route.id })
const { data: { characters }} = useReadQuery(queryRef);
// ...
Thanks for checking this out!
Dang. the only other thing I could think of without diving in is the fact that queryRef includes symbol keys and perhaps these are getting stripped out by the router when returned by the loader (since symbols aren't serializable, this would make sense).
You might consider trying out our TanStack Start integration which is built for streaming SSR but I believe should work with plain TanStack router (@phryneas please correct me if I'm wrong here). I believe that integration will ensure the queryRef is properly serializable to avoid this issue (in case this is the cause of this issue).
For what it’s worth I did give apollo-client-integrations/packages/tanstack-start at main · apollographql/apollo-client-integrations · GitHub a try but to no avail. Here is my implementation if @phryneas wanted to give it a glance.
client.ts
import { createQueryPreloader } from '@apollo/client';
import { ApolloClient, InMemoryCache } from '@apollo/client-integration-tanstack-start';
export const client = new ApolloClient({
cache: new InMemoryCache(),
});
export const preloadQuery = createQueryPreloader(client);
router.ts
import { createRouter as createTanStackRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
import { routerWithApolloClient } from '@apollo/client-integration-tanstack-start';
import { createQueryPreloader } from '@apollo/client';
import { client } from './providers/apollo-provider/client';
export function createRouter() {
const router = createTanStackRouter({
routeTree,
defaultPreload: 'intent',
scrollRestoration: true,
defaultStructuralSharing: true,
// Let Apollo handle caching in loaders instead of Tanstack Router https://tanstack.com/router/v1/docs/framework/react/guide/preloading#preloading-with-external-libraries
defaultPreloadStaleTime: 0,
context: {
// auth will initially be undefined
// We'll be passing down the auth state from within a React component
auth: undefined!,
apolloClient: client,
preloadQuery: createQueryPreloader(client) as any, <--- Couldn't find the correct type for this
},
});
return routerWithApolloClient(router, client);
}
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>;
}
}
app.ts
import { RouterProvider } from '@tanstack/react-router';
import Auth0Provider from './providers/auth0-provider';
import { createRouter } from './router.ts';
import { useAuth0, User } from '@auth0/auth0-react';
import ApolloProvider from './providers/apollo-provider/apollo-provider.tsx';
const router = createRouter();
function InnerApp() {
const auth = useAuth0();
if (auth.isLoading) {
return null;
}
return (
<RouterProvider
router={router} <-- using the client-integration-tanstack-start router
context={{
// There should always be an ID (sub)
auth: auth as ReturnType<typeof useAuth0<User & { sub: NonNullable<User['sub']> }>>,
}}
/>
);
}
export default function App() {
return (
<Auth0Provider
...
>
<ApolloProvider>
<InnerApp />
</ApolloProvider>
</Auth0Provider>
);
}
$dealId.ts
...
loader: ({ params, context }) => {
return context.preloadQuery(DealQuery, {
variables: { dealId: params.dealId },
});
},
...
const dealQueryRef = Route.useLoaderData();
console.log({ dealQueryRef });
const { data } = useReadQuery(dealQueryRef);
This console.log would give me alternating responses between
and
The latter giving me the
Error message
Expected a QueryRef object, but got something else instead.
when I tried to use the queryRef in the useReadQuery
tldr; There was no difference for me in outcome when using the @apollo/client-integrations-tanstack-start and just using @apollo/client
Just to reiterate I am using Vite with React and just TanStack Router, not TanStack Start.
I just pinged Manuel Schiller from the TanStack Router maintainers, and he noted that this could be caused by defaultStructuralSharing - could you try setting this to false?
And I've opened a PR over in TanStack Router that discusses this and suggests different solutions:
https://github.com/TanStack/router/pull/4237
I just pinged Manuel Schiller from the TanStack Router maintainers, and he noted that this could be caused by
defaultStructuralSharing- could you try setting this tofalse?
This worked both when using @apollo/client-integrations-tanstack-start and without 🙏
Great!
Just as a heads-up: you don't need the @apollo/client-integrations-tanstack-start package if you only use @tanstack/router without any of the SSR features of @tanstack/start.
Yeah I'm going to move forward not using the @apollo/client-integrations-tanstack-start package. Let me know if I can help with anything else. It seems that defaultStructuralSharing is a good option to have on so this would be a game changer
Thank you!
@BrennenRocks the TanStack Router PR I have open will probably be merged in the next few days - that said, queryRefs don't benefit at all from structural sharing. A queryRef represents a network request.
Either you have a new queryRef or you don't.
Only if you also passed other values from loaders, those could benefit. With Apollo Client you'd have other ways of reducing rerenders.
I will probably turn on defaultStructuralSharing on a route by route basis when I'm not using useReadQuery
This should now be fixed in TanStack Router 1.120.10, even with defaultStructuralSharing enabled.
Could you please verify that it solves the problem for you?
Confirmed working in @tanstack/[email protected] with defaultStructuralSharing: true at the createRouter level!
@jeremyoverman before I close this as solved, could you please verify that it solves the problem for you too?
We're closing this issue now but feel free to ping the maintainers or open a new issue if you still need support. Thank you!
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. For general questions, we recommend using our Community Forum or Stack Overflow.