React Router framework mode

Integrate Apollo Client with React Router 7


This guide covers integrating Apollo Client in a React Router 7 application with support for modern streaming SSR.

Installation

Install Apollo Client and the React Router integration package:

Bash
1npm install @apollo/client-integration-react-router @apollo/client graphql rxjs

TypeScript users: For type-safe GraphQL operations, see the GraphQL Codegen guide.

Setup

Step 1: Create Apollo configuration

Create an app/apollo.ts file that exports a makeClient function and an apolloLoader:

TypeScript
1import { HttpLink, InMemoryCache } from "@apollo/client";
2import {
3  createApolloLoaderHandler,
4  ApolloClient,
5} from "@apollo/client-integration-react-router";
6
7// `request` will be available on the server during SSR or in loaders, but not in the browser
8export const makeClient = (request?: Request) => {
9  return new ApolloClient({
10    cache: new InMemoryCache(),
11    link: new HttpLink({ uri: "https://your-graphql-endpoint.com/graphql" }),
12  });
13};
14
15export const apolloLoader = createApolloLoaderHandler(makeClient);

Important: ApolloClient must be imported from @apollo/client-integration-react-router, not from @apollo/client.

Step 2: Reveal entry files

Run the following command to create the entry files if they don't exist:

Bash
1npx react-router reveal

This will create app/entry.client.tsx and app/entry.server.tsx.

Step 3: Configure client entry

Adjust app/entry.client.tsx to wrap your app in ApolloProvider:

TypeScript
1import { makeClient } from "./apollo";
2import { ApolloProvider } from "@apollo/client";
3import { StrictMode, startTransition } from "react";
4import { hydrateRoot } from "react-dom/client";
5import { HydratedRouter } from "react-router/dom";
6
7startTransition(() => {
8  const client = makeClient();
9  hydrateRoot(
10    document,
11    <StrictMode>
12      <ApolloProvider client={client}>
13        <HydratedRouter />
14      </ApolloProvider>
15    </StrictMode>
16  );
17});

Step 4: Configure server entry

Adjust app/entry.server.tsx to wrap your app in ApolloProvider during SSR:

TypeScript
1import { makeClient } from "./apollo";
2import { ApolloProvider } from "@apollo/client";
3// ... other imports
4
5export default function handleRequest(
6  request: Request,
7  responseStatusCode: number,
8  responseHeaders: Headers,
9  routerContext: EntryContext
10) {
11  return new Promise((resolve, reject) => {
12    // ... existing code
13
14    const client = makeClient(request);
15
16    const { pipe, abort } = renderToPipeableStream(
17      <ApolloProvider client={client}>
18        <ServerRouter
19          context={routerContext}
20          url={request.url}
21          abortDelay={ABORT_DELAY}
22        />
23      </ApolloProvider>,
24      {
25        [readyOption]() {
26          shellRendered = true;
27          // ... rest of the handler
28        },
29        // ... other options
30      }
31    );
32  });
33}

Step 5: Add hydration helper

Add <ApolloHydrationHelper> to app/root.tsx:

TypeScript
1import { ApolloHydrationHelper } from "@apollo/client-integration-react-router";
2import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
3
4export function Layout({ children }: { children: React.ReactNode }) {
5  return (
6    <html lang="en">
7      <head>
8        <meta charSet="utf-8" />
9        <meta name="viewport" content="width=device-width, initial-scale=1" />
10        <Meta />
11        <Links />
12      </head>
13      <body>
14        <ApolloHydrationHelper>{children}</ApolloHydrationHelper>
15        <ScrollRestoration />
16        <Scripts />
17      </body>
18    </html>
19  );
20}
21
22export default function App() {
23  return <Outlet />;
24}

Usage

Using apolloLoader with useReadQuery

You can now use the apolloLoader function to create Apollo-enabled loaders for your routes:

TypeScript
1import { gql } from "@apollo/client";
2import { useReadQuery } from "@apollo/client/react";
3import { useLoaderData } from "react-router";
4import type { Route } from "./+types/my-route";
5import type { TypedDocumentNode } from "@apollo/client";
6import { apolloLoader } from "./apollo";
7
8// TypedDocumentNode definition with types
9const GET_USER: TypedDocumentNode<
10  { user: { id: string; name: string; email: string } },
11  { id: string }
12> = gql`
13  query GetUser($id: ID!) {
14    user(id: $id) {
15      id
16      name
17      email
18    }
19  }
20`;
21
22export const loader = apolloLoader<Route.LoaderArgs>()(({ preloadQuery }) => {
23  const userQueryRef = preloadQuery(GET_USER, {
24    variables: { id: "1" },
25  });
26
27  return {
28    userQueryRef,
29  };
30});
31
32export default function UserPage() {
33  const { userQueryRef } = useLoaderData<typeof loader>();
34  const { data } = useReadQuery(userQueryRef);
35
36  return (
37    <div>
38      <h1>{data.user.name}</h1>
39      <p>{data.user.email}</p>
40    </div>
41  );
42}

Important: To provide better TypeScript support, apolloLoader is a method that you need to call twice: apolloLoader<LoaderArgs>()(loader)

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 { useLoaderData } from "react-router";
4import type { Route } from "./+types/my-route";
5import { apolloLoader } from "./apollo";
6
7// TypedDocumentNode definitions omitted for brevity
8
9export const loader = apolloLoader<Route.LoaderArgs>()(({ preloadQuery }) => {
10  const userQueryRef = preloadQuery(GET_USER, {
11    variables: { id: "1" },
12  });
13
14  const postsQueryRef = preloadQuery(GET_POSTS, {
15    variables: { userId: "1" },
16  });
17
18  return {
19    userQueryRef,
20    postsQueryRef,
21  };
22});
23
24export default function UserPage() {
25  const { userQueryRef, postsQueryRef } = useLoaderData<typeof loader>();
26  const { data: userData } = useReadQuery(userQueryRef);
27  const { data: postsData } = useReadQuery(postsQueryRef);
28
29  return (
30    <div>
31      <h1>{userData.user.name}</h1>
32      <h2>Posts</h2>
33      <ul>
34        {postsData.posts.map((post) => (
35          <li key={post.id}>{post.title}</li>
36        ))}
37      </ul>
38    </div>
39  );
40}

Important considerations

  1. Import ApolloClient from integration package: Always import ApolloClient from @apollo/client-integration-react-router, not from @apollo/client, to ensure proper SSR hydration.

  2. TypeScript support: The apolloLoader function requires double invocation for proper TypeScript type inference: apolloLoader<LoaderArgs>()(loader).

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

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

  5. Cache hydration: The ApolloHydrationHelper component ensures that data loaded on the server is properly hydrated on the client, preventing unnecessary refetches.

Feedback

Edit on GitHub

Ask Community