docs/source/integrations/react-router.mdx
This guide covers integrating Apollo Client in a React Router 7 application with support for modern streaming SSR.
Install Apollo Client and the React Router integration package:
npm install @apollo/client-integration-react-router @apollo/client graphql rxjs
TypeScript users: For type-safe GraphQL operations, see the GraphQL Codegen guide.
Create an app/apollo.ts file that exports a makeClient function and an apolloLoader:
import { HttpLink, InMemoryCache } from "@apollo/client";
import {
createApolloLoaderHandler,
ApolloClient,
} from "@apollo/client-integration-react-router";
// `request` will be available on the server during SSR or in loaders, but not in the browser
export const makeClient = (request?: Request) => {
return new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({ uri: "https://your-graphql-endpoint.com/graphql" }),
});
};
export const apolloLoader = createApolloLoaderHandler(makeClient);
Important:
ApolloClientmust be imported from@apollo/client-integration-react-router, not from@apollo/client.
Run the following command to create the entry files if they don't exist:
npx react-router reveal
This will create app/entry.client.tsx and app/entry.server.tsx.
Adjust app/entry.client.tsx to wrap your app in ApolloProvider:
import { makeClient } from "./apollo";
import { ApolloProvider } from "@apollo/client";
import { StrictMode, startTransition } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
startTransition(() => {
const client = makeClient();
hydrateRoot(
document,
<StrictMode>
<ApolloProvider client={client}>
<HydratedRouter />
</ApolloProvider>
</StrictMode>
);
});
Adjust app/entry.server.tsx to wrap your app in ApolloProvider during SSR:
import { makeClient } from "./apollo";
import { ApolloProvider } from "@apollo/client";
// ... other imports
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext
) {
return new Promise((resolve, reject) => {
// ... existing code
const client = makeClient(request);
const { pipe, abort } = renderToPipeableStream(
<ApolloProvider client={client}>
<ServerRouter
context={routerContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>
</ApolloProvider>,
{
[readyOption]() {
shellRendered = true;
// ... rest of the handler
},
// ... other options
}
);
});
}
Add <ApolloHydrationHelper> to app/root.tsx:
import { ApolloHydrationHelper } from "@apollo/client-integration-react-router";
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<ApolloHydrationHelper>{children}</ApolloHydrationHelper>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
You can now use the apolloLoader function to create Apollo-enabled loaders for your routes:
import { gql } from "@apollo/client";
import { useReadQuery } from "@apollo/client/react";
import { useLoaderData } from "react-router";
import type { Route } from "./+types/my-route";
import type { TypedDocumentNode } from "@apollo/client";
import { apolloLoader } from "./apollo";
// 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 loader = apolloLoader<Route.LoaderArgs>()(({ preloadQuery }) => {
const userQueryRef = preloadQuery(GET_USER, {
variables: { id: "1" },
});
return {
userQueryRef,
};
});
export default function UserPage() {
const { userQueryRef } = useLoaderData<typeof loader>();
const { data } = useReadQuery(userQueryRef);
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
</div>
);
}
Important: To provide better TypeScript support,
apolloLoaderis a method that you need to call twice:apolloLoader<LoaderArgs>()(loader)
You can preload multiple queries in a single loader:
import { gql } from "@apollo/client";
import { useReadQuery } from "@apollo/client/react";
import { useLoaderData } from "react-router";
import type { Route } from "./+types/my-route";
import { apolloLoader } from "./apollo";
// TypedDocumentNode definitions omitted for brevity
export const loader = apolloLoader<Route.LoaderArgs>()(({ preloadQuery }) => {
const userQueryRef = preloadQuery(GET_USER, {
variables: { id: "1" },
});
const postsQueryRef = preloadQuery(GET_POSTS, {
variables: { userId: "1" },
});
return {
userQueryRef,
postsQueryRef,
};
});
export default function UserPage() {
const { userQueryRef, postsQueryRef } = useLoaderData<typeof loader>();
const { data: userData } = useReadQuery(userQueryRef);
const { data: postsData } = useReadQuery(postsQueryRef);
return (
<div>
<h1>{userData.user.name}</h1>
<h2>Posts</h2>
<ul>
{postsData.posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
Import ApolloClient from integration package: Always import ApolloClient from @apollo/client-integration-react-router, not from @apollo/client, to ensure proper SSR hydration.
TypeScript support: The apolloLoader function requires double invocation for proper TypeScript type inference: apolloLoader<LoaderArgs>()(loader).
Request context: The makeClient function receives the Request object during SSR and in loaders, but not in the browser. Use this to set up auth headers or other request-specific configuration.
Streaming SSR: The integration fully supports React's streaming SSR capabilities. Place Suspense boundaries strategically for optimal user experience.
Cache hydration: The ApolloHydrationHelper component ensures that data loaded on the server is properly hydrated on the client, preventing unnecessary refetches.