@apollo/client
Version:
A fully-featured caching GraphQL client.
379 lines (305 loc) • 11.2 kB
Markdown
# Apollo Client Integration 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
npm install @apollo/client-integration-tanstack-start @apollo/client graphql rxjs
```
> **TypeScript users:** For type-safe GraphQL operations, see the [TypeScript Code Generation guide](typescript-codegen.md).
## 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
import type { ApolloClientIntegration } from "@apollo/client-integration-tanstack-start";
import {
createRootRouteWithContext,
Outlet,
} from "@tanstack/react-router";
export const Route = createRootRouteWithContext<ApolloClientIntegration.RouterContext>()({
component: RootComponent,
});
function RootComponent() {
return (
<html>
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
</head>
<body>
<Outlet />
</body>
</html>
);
}
```
### Step 2: Set Up Apollo Client in Router
In your `router.tsx`, set up your Apollo Client instance and run `routerWithApolloClient`:
```typescript
import {
routerWithApolloClient,
ApolloClient,
InMemoryCache,
} from "@apollo/client-integration-tanstack-start";
import { HttpLink } from "@apollo/client";
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
export function getRouter() {
const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({ uri: "https://your-graphql-endpoint.com/graphql" }),
});
const router = createRouter({
routeTree,
context: {
...routerWithApolloClient.defaultContext,
},
});
return routerWithApolloClient(router, apolloClient);
}
```
> **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
import { gql } from "@apollo/client";
import { useReadQuery } from "@apollo/client/react";
import { createFileRoute } from "@tanstack/react-router";
import type { TypedDocumentNode } from "@apollo/client";
// TypedDocumentNode definition with types
const GET_USER: TypedDocumentNode<
{ user: { id: string; name: string; email: string } },
{ id: string }
> = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
export const Route = createFileRoute("/user/$userId")({
component: RouteComponent,
loader: ({ context: { preloadQuery }, params }) => {
const queryRef = preloadQuery(GET_USER, {
variables: { id: params.userId },
});
return {
queryRef,
};
},
});
function RouteComponent() {
const { queryRef } = Route.useLoaderData();
const { data } = useReadQuery(queryRef);
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
</div>
);
}
```
### Option 2: Direct useSuspenseQuery in Component
You can also use Apollo Client's suspenseful hooks directly in your component without a loader:
```typescript
import { gql } from "@apollo/client";
import { useSuspenseQuery } from "@apollo/client/react";
import { createFileRoute } from "@tanstack/react-router";
import type { TypedDocumentNode } from "@apollo/client";
// TypedDocumentNode definition with types
const GET_POSTS: TypedDocumentNode<{
posts: Array<{ id: string; title: string; content: string }>;
}> = gql`
query GetPosts {
posts {
id
title
content
}
}
`;
export const Route = createFileRoute("/posts")({
component: RouteComponent,
});
function RouteComponent() {
const { data } = useSuspenseQuery(GET_POSTS);
return (
<div>
<h1>Posts</h1>
<ul>
{data.posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</li>
))}
</ul>
</div>
);
}
```
### Multiple Queries in a Loader
You can preload multiple queries in a single loader:
```typescript
import { gql } from "@apollo/client";
import { useReadQuery } from "@apollo/client/react";
import { createFileRoute } from "@tanstack/react-router";
// TypedDocumentNode definitions omitted for brevity
export const Route = createFileRoute("/dashboard")({
component: RouteComponent,
loader: ({ context: { preloadQuery } }) => {
const userQueryRef = preloadQuery(GET_USER, {
variables: { id: "current" },
});
const statsQueryRef = preloadQuery(GET_STATS, {
variables: { period: "month" },
});
return {
userQueryRef,
statsQueryRef,
};
},
});
function RouteComponent() {
const { userQueryRef, statsQueryRef } = Route.useLoaderData();
const { data: userData } = useReadQuery(userQueryRef);
const { data: statsData } = useReadQuery(statsQueryRef);
return (
<div>
<h1>Welcome, {userData.user.name}</h1>
<div>
<h2>Monthly Stats</h2>
<p>Views: {statsData.stats.views}</p>
<p>Clicks: {statsData.stats.clicks}</p>
</div>
</div>
);
}
```
### 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
import { useReadQuery, useQueryRefHandlers, QueryRef } from "@apollo/client/react";
function UserComponent({ queryRef }: { queryRef: QueryRef<GetUserQuery> }) {
const { refetch } = useQueryRefHandlers(queryRef);
const { data } = useReadQuery(queryRef);
return (
<div>
<h1>{data.user.name}</h1>
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}
```
## 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
import {
ApolloClient,
InMemoryCache,
routerWithApolloClient,
} from "@apollo/client-integration-tanstack-start";
import { ApolloLink, HttpLink } from "@apollo/client";
import { SetContextLink } from "@apollo/client/link/context";
import { createIsomorphicFn } from "@tanstack/react-start";
import { createRouter } from "@tanstack/react-router";
import { getSession, getCookie } from "@tanstack/react-start/server";
import { routeTree } from "./routeTree.gen";
// Create isomorphic link that uses different implementations per environment
const createAuthLink = createIsomorphicFn()
.server(() => {
// Server-only: Can access server-side functions like `getCookies`, `getCookie`, `getSession`, etc. exported from `"@tanstack/react-start/server"`
return new SetContextLink(async (prevContext) => {
return {
headers: {
...prevContext.headers,
authorization: getCookie("Authorization"),
},
};
});
})
.client(() => {
// Client-only: Can access `localStorage` or other browser APIs
return new SetContextLink((prevContext) => {
return {
headers: {
...prevContext.headers,
authorization: localStorage.getItem("authToken") ?? "",
},
};
});
});
export function getRouter() {
const httpLink = new HttpLink({
uri: "https://your-graphql-endpoint.com/graphql",
});
const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: ApolloLink.from([createAuthLink(), httpLink]),
});
const router = createRouter({
routeTree,
context: {
...routerWithApolloClient.defaultContext,
},
});
return routerWithApolloClient(router, apolloClient);
}
```
> **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
import {
ApolloClient,
InMemoryCache,
} from "@apollo/client-integration-tanstack-start";
import { HttpLink } from "@apollo/client";
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
import { routerWithApolloClient } from "@apollo/client-integration-tanstack-start";
export function getRouter() {
const apolloClient = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
}),
link: new HttpLink({ uri: "https://your-graphql-endpoint.com/graphql" }),
});
const router = createRouter({
routeTree,
context: {
...routerWithApolloClient.defaultContext,
},
});
return routerWithApolloClient(router, apolloClient);
}
```