Server-side rendering
Server-side rendering (SSR) is a performance optimization for modern web apps. It enables you to render your app's initial state to raw HTML and CSS on the server before serving it to a browser. This means users don't have to wait for their browser to download and initialize React (or Angular, Vue, etc.) before content is available:
Apollo Client provides a handy API for using it with server-side rendering, including a function that executes all of the GraphQL queries that are required to render your component tree. You don't need to make any changes to your queries to support this API.
Differences from client-side rendering
When you render your React app on the server side, most of the code is identical to its client-side counterpart, with a few important exceptions:
You need to use a server-compatible router for React, such as React Router.
(In the case of React Router, you wrap your application in a
StaticRoutercomponent instead of theBrowserRouteryou use on the client side.)You need to replace relative URLs with absolute URLs wherever applicable.
The initialization of Apollo Client changes slightly, as described below.
Initializing Apollo Client
Here's an example server-side initialization of Apollo Client:
1import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
2
3const client = new ApolloClient({
4 ssrMode: true,
5 link: new HttpLink({
6 uri: "http://localhost:3010",
7 credentials: "same-origin",
8 headers: {
9 cookie: req.header("Cookie"),
10 },
11 }),
12 cache: new InMemoryCache(),
13});Provide ssrMode: true to prevent Apollo Client from polling on the server. This setting also tells the client to prioritize cache values over network requests when possible.
You also might need to configure your GraphQL endpoint to accept GraphQL operations from your SSR server (for example, by safelisting its domain or IP). Use absolute URLs for your GraphQL endpoint on the server, because relative network requests can only be made in a browser.
It's possible and valid for your GraphQL endpoint to be hosted by the same server that's performing SSR. In this case, Apollo Client doesn't need to make network requests to execute queries. For details, see Avoiding the network for local queries.
Example
Let's look at an example of SSR in a Node.js app. This example uses Express and React Router v4, although it can work with any server middleware and any router that supports SSR.
First, here's an example app.js file, without the code for rendering React to HTML and CSS:
Click to expand
1import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
2import { ApolloProvider } from "@apollo/client/react";
3import Express from "express";
4import React from "react";
5import { StaticRouter } from "react-router";
6
7// File shown below
8import Layout from "./routes/Layout";
9
10const app = new Express();
11app.use((req, res) => {
12 const client = new ApolloClient({
13 ssrMode: true,
14 link: new HttpLink({
15 uri: "http://localhost:3010",
16 credentials: "same-origin",
17 headers: {
18 cookie: req.header("Cookie"),
19 },
20 }),
21 cache: new InMemoryCache(),
22 });
23
24 const context = {};
25
26 // The client-side App will instead use <BrowserRouter>
27 const App = (
28 <ApolloProvider client={client}>
29 <StaticRouter location={req.url} context={context}>
30 <Layout />
31 </StaticRouter>
32 </ApolloProvider>
33 );
34
35 // TODO: rendering code (see below)
36});
37
38app.listen(basePort, () =>
39 console.log(`app Server is now running on http://localhost:${basePort}`)
40);So far, whenever this example server receives a request, it first initializes Apollo Client and then creates a React tree that's wrapped with the ApolloProvider and StaticRouter components. The contents of that tree depend on the request's path and the StaticRouter's defined routes.
Executing queries with prerenderStatic
You can instruct Apollo Client to execute all of the queries executed by the useQuery or the suspenseful query hooks (like useSuspenseQuery and useBackgroundQuery) in the React tree's components with the prerenderStatic function.
This function rerenders your React tree until no more network requests are made. When you use suspenseful hooks with a suspense-ready rendering function, the tree is rendered once and suspends while network requests are executed. When you use non-suspenseful hooks (like useQuery), this function renders all components, waits for all requests to finish, and then re-renders the tree until no more requests are made.
The function returns a Promise that resolves when all result data is ready in the Apollo Client cache and the final render is complete.
Choosing a rendering function
prerenderStatic supports multiple React rendering functions:
prerenderfromreact-dom/static- Recommended for Deno or modern edge runtimes with Web Streams. Supports React Suspense.prerenderToNodeStreamfromreact-dom/static- Recommended for Node.js. Supports React Suspense.renderToStringfromreact-dom/server- Legacy API without Suspense support. Won't work with suspenseful hooks.renderToStaticMarkupfromreact-dom/server- Legacy API without Suspense support. Slightly faster thanrenderToString, but the result cannot be hydrated.
The following code replaces the TODO comment within the app.use call in the example above:
1// Add these imports to the top of the file
2import { prerenderStatic } from "@apollo/client/react/ssr";
3import { prerenderToNodeStream } from "react-dom/static";
4
5// Replace the TODO with this
6prerenderStatic({
7 tree: App,
8 // this is optional if your `App` component contains an <ApolloProvider>
9 context: { client },
10 renderFunction: prerenderToNodeStream,
11}).then(async ({ result }) => {
12 // Extract the entirety of the Apollo Client cache's current state
13 const initialState = client.extract();
14
15 // TODO: Send the response to the client (see below for examples)
16});Sending the response
After prerenderStatic completes, you need to send the HTML response to the client. The approach depends on the rendering function you chose.
Using streaming with renderToPipeableStream
For Node.js environments, you can stream the response to the client using renderToPipeableStream. This allows the browser to start displaying content before the entire page is rendered:
1import { renderToPipeableStream } from "react-dom/server";
2
3// After prerenderStatic completes
4prerenderStatic({
5 tree: App,
6 context: { client },
7 renderFunction: prerenderToNodeStream,
8}).then(async ({ result }) => {
9 const initialState = client.extract();
10
11 // Render the app again with streaming, injecting the Apollo state
12 const { pipe } = renderToPipeableStream(
13 <html>
14 <head>
15 <title>My App</title>
16 </head>
17 <body>
18 <div id="root">
19 <App />
20 </div>
21 </body>
22 </html>,
23 {
24 bootstrapScriptContent: `window.__APOLLO_STATE__=${JSON.stringify(
25 initialState
26 ).replace(/</g, "\\u003c")}`,
27 bootstrapScripts: ["/client.js"],
28 onShellReady() {
29 // Start streaming the response to the browser
30 res.setHeader("Content-Type", "text/html");
31 res.statusCode = 200;
32 pipe(res);
33 },
34 onError(error) {
35 console.error("Rendering error:", error);
36 },
37 }
38 );
39});<div id="root"><App /></div>, you can also render <div id="root" dangerouslySetInnerHTML={{ __html: result }} /> to avoid rendering the entire tree twice.
However, if you go this approach, React cannot decide on the order in which components are streamed to the browser, which might be suboptimal.
In the end, it's a tradeoff between more work on the server and a potential delay in displaying content in the browser that you need to make based on your individual requirements.useSuspenseQuery or useBackgroundQuery) and no useQuery hooks, is also possible to use the @apollo/client-react-streaming package to stream data dynamically while it is fetching, without requiring a first render pass with prerenderStatic.
You can find an example application that uses this approach in the integration tests for that package.replace call in these examples escapes the < character to prevent cross-site scripting attacks that are possible via the presence of </script> in a string literal.Using renderToString for synchronous rendering
For simpler use cases or environments without stream support, you can use renderToString to render the entire page synchronously:
1import { renderToString } from "react-dom/server";
2
3// After prerenderStatic completes
4prerenderStatic({
5 tree: App,
6 context: { client },
7 renderFunction: renderToString,
8}).then(async ({ result }) => {
9 const initialState = client.extract();
10
11 // Create a complete HTML document with the cache state
12 const Html = () => (
13 <html>
14 <head>
15 <title>My App</title>
16 </head>
17 <body>
18 <div id="root" dangerouslySetInnerHTML={{ __html: result }} />
19 <script
20 dangerouslySetInnerHTML={{
21 __html: `window.__APOLLO_STATE__=${JSON.stringify(
22 initialState
23 ).replace(/</g, "\\u003c")};`,
24 }}
25 />
26 <script src="/client.js" />
27 </body>
28 </html>
29 );
30
31 // Render to string and send
32 const html = renderToString(<Html />);
33
34 res.setHeader("Content-Type", "text/html");
35 res.status(200);
36 res.send(`<!DOCTYPE html>${html}`);
37 res.end();
38});replace call in these examples escapes the < character to prevent cross-site scripting attacks that are possible via the presence of </script> in a string literal.Advanced options
prerenderStatic provides several options to customize the rendering process:
Diagnostics
You can enable diagnostics to detect inefficient rendering structures (like useQuery waterfalls) in your app:
1const { diagnostics } = await prerenderStatic({
2 tree: App,
3 context: { client },
4 renderFunction: prerenderToNodeStream,
5 diagnostics: true,
6});
7
8console.log(`Rendered ${diagnostics.renderCount} times`);
9// If renderCount is high, consider using fragment colocationTimeout support
You can use an AbortSignal to stop the render loop early:
1const signal = AbortSignal.timeout(2000); // 2 second timeout
2
3const { result, aborted } = await prerenderStatic({
4 tree: App,
5 context: { client },
6 renderFunction: (tree) => prerenderToNodeStream(tree, { signal }),
7 signal,
8});
9
10if (aborted) {
11 console.log("Render timed out, returning partial result");
12}Maximum rerenders
If you have deep useQuery waterfalls, you can increase the maxRerenders option (default: 50):
1await prerenderStatic({
2 tree: App,
3 context: { client },
4 renderFunction: prerenderToNodeStream,
5 maxRerenders: 100,
6});Rehydrating the client-side cache
Although the server-side cache's state is available in __APOLLO_STATE__, it isn't yet available in the client-side cache. InMemoryCache provides a helpful restore function for rehydrating its state with data extracted from another cache instance.
In your client-side initialization of Apollo Client, you can rehydrate the cache like so:
1const client = new ApolloClient({
2 cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
3 uri: "https://example.com/graphql",
4});Now when the client-side version of the app runs its initial queries, the data is returned instantly because it's already in the cache!
Overriding fetch policies during initialization
If some of your initial queries use the network-only or cache-and-network fetch policy, you can provide the ssrForceFetchDelay option to Apollo Client to skip force-fetching those queries during initialization. This way, even those queries initially run using only the cache:
1const client = new ApolloClient({
2 cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
3 link,
4 ssrForceFetchDelay: 100, // in milliseconds
5});Avoiding the network for local queries
If your GraphQL endpoint is hosted by the same server that you're rendering from, you can optionally avoid using the network when executing your SSR queries. This is particularly helpful if localhost is firewalled in the server's environment (e.g., on Heroku).
When creating an Apollo Client on the server, use a SchemaLink instead of an HttpLink. SchemaLink uses your schema and context to run the query immediately, without the need for a network request:
1import { ApolloClient, InMemoryCache } from "@apollo/client";
2import { SchemaLink } from "@apollo/client/link/schema";
3
4// ...
5
6const client = new ApolloClient({
7 // Instead of HttpLink use SchemaLink here
8 link: new SchemaLink({ schema }),
9 cache: new InMemoryCache(),
10});Selectively disabling query execution during SSR
If you want to prevent a particular query from executing during SSR, use ssr: false in that query's options. This is useful for queries that should only run on the client side, such as user-specific data that isn't available during SSR.
When ssr: false is set, the component receives a result with loading: true, dataState: "empty", and data: undefined during server-side rendering. The query will execute normally once the component hydrates on the client.
1function ClientOnlyUser() {
2 const { loading, data } = useQuery(GET_USER_WITH_ID, { ssr: false });
3
4 if (loading) {
5 return <span>Loading...</span>;
6 }
7
8 return <span>User: {data?.user?.name || "Not loaded"}</span>;
9}ssr: false behaves differently than skip: true, which prevents the query from executing on both the server and client until skip is set to false. During SSR, skip: true results in loading: false and networkStatus: NetworkStatus.ready.Legacy APIs
Apollo Client provides two legacy SSR functions that are both replaced by prerenderStatic:
getDataFromTree- Executes all queries in a component tree and returns when data is ready. UsesrenderToStaticMarkupby default.renderToStringWithData- Similar togetDataFromTree, but returns the rendered string usingrenderToString.
These functions are deprecated. Use prerenderStatic instead, which offers better flexibility and performance with modern React rendering APIs.