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-app to 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:

Bash
1npm install @apollo/client-integration-tanstack-start @apollo/client graphql rxjs

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

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

TypeScript
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: ApolloClient and InMemoryCache must 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:

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

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

TypeScript
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 useQueryRefHandlers before useReadQuery. These two hooks interact with the same queryRef, and calling them in the wrong order could cause subtle bugs.

TypeScript
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

  1. Import from integration package: Always import ApolloClient and InMemoryCache from @apollo/client-integration-tanstack-start, not from @apollo/client, to ensure proper SSR hydration.

  2. Context type: Use createRootRouteWithContext<ApolloClientIntegration.RouterContext>() to provide proper TypeScript types for the preloadQuery function in loaders.

  3. Loader vs component queries:

    • Use preloadQuery in loaders when you want to start fetching data before the component renders

    • Use useSuspenseQuery directly in components for simpler cases or when data fetching can wait until render

  4. Streaming SSR: The integration fully supports React's streaming SSR capabilities. Place Suspense boundaries strategically for optimal user experience.

  5. 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.

  6. Authentication: Use Apollo Client's SetContextLink for 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:

TypeScript
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 getRouter function is called both on the server and client, so it must not contain environment-specific code. Use createIsomorphicFn to provide different implementations:

  • Server: Can access server-only functions like getSession, getCookies, getCookie from @tanstack/react-start/server to access authentication information in request or session data

  • Client: Can use localStorage or other browser APIs to access auth tokens (if setting credentials: "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

TypeScript
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}
Feedback

Edit on GitHub

Ask Community