TanStack Start
Integrate Apollo Client with TanStack Start
This guide covers integrating Apollo Client in a TanStack Start application with support for modern streaming SSR.
Note: When using
npx create-tsrouter-appto create a new TanStack Start application, you can choose Apollo Client in the setup wizard to have all of this configuration automatically set up for you.
Installation
Install Apollo Client and the TanStack Start integration package:
1npm install @apollo/client-integration-tanstack-start @apollo/client graphql rxjsTypeScript users: For type-safe GraphQL operations, see the GraphQL Codegen guide.
Setup
Step 1: Configure root route with context
In your routes/__root.tsx, change from createRootRoute to createRootRouteWithContext to provide the right context type:
1import type { ApolloClientIntegration } from "@apollo/client-integration-tanstack-start";
2import {
3 createRootRouteWithContext,
4 Outlet,
5} from "@tanstack/react-router";
6
7export const Route = createRootRouteWithContext<ApolloClientIntegration.RouterContext>()({
8 component: RootComponent,
9});
10
11function RootComponent() {
12 return (
13 <html>
14 <head>
15 <meta charSet="UTF-8" />
16 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
17 <title>My App</title>
18 </head>
19 <body>
20 <Outlet />
21 </body>
22 </html>
23 );
24}Step 2: Set up Apollo Client in router
In your router.tsx, set up your Apollo Client instance and run routerWithApolloClient:
1import {
2 routerWithApolloClient,
3 ApolloClient,
4 InMemoryCache,
5} from "@apollo/client-integration-tanstack-start";
6import { HttpLink } from "@apollo/client";
7import { createRouter } from "@tanstack/react-router";
8import { routeTree } from "./routeTree.gen";
9
10export function getRouter() {
11 const apolloClient = new ApolloClient({
12 cache: new InMemoryCache(),
13 link: new HttpLink({ uri: "https://your-graphql-endpoint.com/graphql" }),
14 });
15
16 const router = createRouter({
17 routeTree,
18 context: {
19 ...routerWithApolloClient.defaultContext,
20 },
21 });
22
23 return routerWithApolloClient(router, apolloClient);
24}Important:
ApolloClientandInMemoryCachemust be imported from@apollo/client-integration-tanstack-start, not from@apollo/client.
Usage
Option 1: Loader with preloadQuery and useReadQuery
Use the preloadQuery function in your route loader to preload data during navigation:
1import { gql } from "@apollo/client";
2import { useReadQuery } from "@apollo/client/react";
3import { createFileRoute } from "@tanstack/react-router";
4import type { TypedDocumentNode } from "@apollo/client";
5
6// TypedDocumentNode definition with types
7const GET_USER: TypedDocumentNode<
8 { user: { id: string; name: string; email: string } },
9 { id: string }
10> = gql`
11 query GetUser($id: ID!) {
12 user(id: $id) {
13 id
14 name
15 email
16 }
17 }
18`;
19
20export const Route = createFileRoute("/user/$userId")({
21 component: RouteComponent,
22 loader: ({ context: { preloadQuery }, params }) => {
23 const queryRef = preloadQuery(GET_USER, {
24 variables: { id: params.userId },
25 });
26
27 return {
28 queryRef,
29 };
30 },
31});
32
33function RouteComponent() {
34 const { queryRef } = Route.useLoaderData();
35 const { data } = useReadQuery(queryRef);
36
37 return (
38 <div>
39 <h1>{data.user.name}</h1>
40 <p>{data.user.email}</p>
41 </div>
42 );
43}Option 2: Direct useSuspenseQuery in component
You can also use Apollo Client's suspenseful hooks directly in your component without a loader:
1import { gql, useSuspenseQuery } from "@apollo/client/react";
2import { createFileRoute } from "@tanstack/react-router";
3import type { TypedDocumentNode } from "@apollo/client";
4
5// TypedDocumentNode definition with types
6const GET_POSTS: TypedDocumentNode<{
7 posts: Array<{ id: string; title: string; content: string }>;
8}> = gql`
9 query GetPosts {
10 posts {
11 id
12 title
13 content
14 }
15 }
16`;
17
18export const Route = createFileRoute("/posts")({
19 component: RouteComponent,
20});
21
22function RouteComponent() {
23 const { data } = useSuspenseQuery(GET_POSTS);
24
25 return (
26 <div>
27 <h1>Posts</h1>
28 <ul>
29 {data.posts.map((post) => (
30 <li key={post.id}>
31 <h2>{post.title}</h2>
32 <p>{post.content}</p>
33 </li>
34 ))}
35 </ul>
36 </div>
37 );
38}Multiple queries in a loader
You can preload multiple queries in a single loader:
1import { gql } from "@apollo/client";
2import { useReadQuery } from "@apollo/client/react";
3import { createFileRoute } from "@tanstack/react-router";
4
5// TypedDocumentNode definitions omitted for brevity
6
7export const Route = createFileRoute("/dashboard")({
8 component: RouteComponent,
9 loader: ({ context: { preloadQuery } }) => {
10 const userQueryRef = preloadQuery(GET_USER, {
11 variables: { id: "current" },
12 });
13
14 const statsQueryRef = preloadQuery(GET_STATS, {
15 variables: { period: "month" },
16 });
17
18 return {
19 userQueryRef,
20 statsQueryRef,
21 };
22 },
23});
24
25function RouteComponent() {
26 const { userQueryRef, statsQueryRef } = Route.useLoaderData();
27 const { data: userData } = useReadQuery(userQueryRef);
28 const { data: statsData } = useReadQuery(statsQueryRef);
29
30 return (
31 <div>
32 <h1>Welcome, {userData.user.name}</h1>
33 <div>
34 <h2>Monthly Stats</h2>
35 <p>Views: {statsData.stats.views}</p>
36 <p>Clicks: {statsData.stats.clicks}</p>
37 </div>
38 </div>
39 );
40}Using useQueryRefHandlers for refetching
When using useReadQuery, you can get refetch functionality from useQueryRefHandlers:
Important: Always call
useQueryRefHandlersbeforeuseReadQuery. These two hooks interact with the samequeryRef, and calling them in the wrong order could cause subtle bugs.
1import { useReadQuery, useQueryRefHandlers, QueryRef } from "@apollo/client/react";
2
3function UserComponent({ queryRef }: { queryRef: QueryRef<GetUserQuery> }) {
4 const { refetch } = useQueryRefHandlers(queryRef);
5 const { data } = useReadQuery(queryRef);
6
7 return (
8 <div>
9 <h1>{data.user.name}</h1>
10 <button onClick={() => refetch()}>Refresh</button>
11 </div>
12 );
13}Important considerations
Import from integration package: Always import
ApolloClientandInMemoryCachefrom@apollo/client-integration-tanstack-start, not from@apollo/client, to ensure proper SSR hydration.Context type: Use
createRootRouteWithContext<ApolloClientIntegration.RouterContext>()to provide proper TypeScript types for thepreloadQueryfunction in loaders.Loader vs component queries:
Use
preloadQueryin loaders when you want to start fetching data before the component rendersUse
useSuspenseQuerydirectly in components for simpler cases or when data fetching can wait until render
Streaming SSR: The integration fully supports React's streaming SSR capabilities. Place
Suspenseboundaries strategically for optimal user experience.Cache management: The Apollo Client instance is shared across all routes, so cache updates from one route will be reflected in all routes that use the same data.
Authentication: Use Apollo Client's
SetContextLinkfor dynamic auth tokens.
Advanced configuration
Adding authentication
For authentication in TanStack Start with SSR support, you need to handle both server and client environments differently. Use createIsomorphicFn to provide environment-specific implementations:
1import {
2 ApolloClient,
3 InMemoryCache,
4 routerWithApolloClient,
5} from "@apollo/client-integration-tanstack-start";
6import { ApolloLink, HttpLink } from "@apollo/client";
7import { SetContextLink } from "@apollo/client/link/context";
8import { createIsomorphicFn } from "@tanstack/react-start";
9import { createRouter } from "@tanstack/react-router";
10import { getSession, getCookie } from "@tanstack/react-start/server";
11import { routeTree } from "./routeTree.gen";
12
13// Create isomorphic link that uses different implementations per environment
14const createAuthLink = createIsomorphicFn()
15 .server(() => {
16 // Server-only: Can access server-side functions like `getCookies`, `getCookie`, `getSession`, etc. exported from `"@tanstack/react-start/server"`
17 return new SetContextLink(async (prevContext) => {
18 return {
19 headers: {
20 ...prevContext.headers,
21 authorization: getCookie("Authorization"),
22 },
23 };
24 });
25 })
26 .client(() => {
27 // Client-only: Can access `localStorage` or other browser APIs
28 return new SetContextLink((prevContext) => {
29 return {
30 headers: {
31 ...prevContext.headers,
32 authorization: localStorage.getItem("authToken") ?? "",
33 },
34 };
35 });
36 });
37
38export function getRouter() {
39 const httpLink = new HttpLink({
40 uri: "https://your-graphql-endpoint.com/graphql",
41 });
42
43 const apolloClient = new ApolloClient({
44 cache: new InMemoryCache(),
45 link: ApolloLink.from([createAuthLink(), httpLink]),
46 });
47
48 const router = createRouter({
49 routeTree,
50 context: {
51 ...routerWithApolloClient.defaultContext,
52 },
53 });
54
55 return routerWithApolloClient(router, apolloClient);
56}Important: The
getRouterfunction is called both on the server and client, so it must not contain environment-specific code. UsecreateIsomorphicFnto provide different implementations:
Server: Can access server-only functions like
getSession,getCookies,getCookiefrom@tanstack/react-start/serverto access authentication information in request or session dataClient: Can use
localStorageor other browser APIs to access auth tokens (if settingcredentials: "include"is sufficient, try to prefer that over manually setting auth headers client-side)This ensures your authentication works correctly in both SSR and browser contexts.
Custom cache configuration
1import {
2 ApolloClient,
3 InMemoryCache,
4} from "@apollo/client-integration-tanstack-start";
5import { HttpLink } from "@apollo/client";
6import { createRouter } from "@tanstack/react-router";
7import { routeTree } from "./routeTree.gen";
8import { routerWithApolloClient } from "@apollo/client-integration-tanstack-start";
9
10export function getRouter() {
11 const apolloClient = new ApolloClient({
12 cache: new InMemoryCache({
13 typePolicies: {
14 Query: {
15 fields: {
16 posts: {
17 merge(existing = [], incoming) {
18 return [...existing, ...incoming];
19 },
20 },
21 },
22 },
23 },
24 }),
25 link: new HttpLink({ uri: "https://your-graphql-endpoint.com/graphql" }),
26 });
27
28 const router = createRouter({
29 routeTree,
30 context: {
31 ...routerWithApolloClient.defaultContext,
32 },
33 });
34
35 return routerWithApolloClient(router, apolloClient);
36}