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 StaticRouter component instead of the BrowserRouter you 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:

JavaScript
1import {
2  ApolloClient,
3  createHttpLink,
4  InMemoryCache
5} from '@apollo/client';
6
7const client = new ApolloClient({
8  ssrMode: true,
9  link: createHttpLink({
10    uri: 'http://localhost:3010',
11    credentials: 'same-origin',
12    headers: {
13      cookie: req.header('Cookie'),
14    },
15  }),
16  cache: new InMemoryCache(),
17});

You'll notice a couple differences from a typical client-side initialization:

  • You provide ssrMode: true. This prevents Apollo Client from refetching queries unnecessarily, and it also enables you to use the getDataFromTree function (covered below).

  • Instead of providing a uri option, you provide an HttpLink instance to the link option. This enables you to specify any required authentication details when sending requests to your GraphQL endpoint from the server side.

    Note that you also might need to make sure your GraphQL endpoint is configured to accept GraphQL operations from your SSR server (for example, by safelisting its domain or IP).

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
JavaScript
app.js
1import {
2  ApolloProvider,
3  ApolloClient,
4  createHttpLink,
5  InMemoryCache
6} from '@apollo/client';
7import Express from 'express';
8import React from 'react';
9import { StaticRouter } from 'react-router';
10
11// File shown below
12import Layout from './routes/Layout';
13
14const app = new Express();
15app.use((req, res) => {
16
17  const client = new ApolloClient({
18    ssrMode: true,
19    link: createHttpLink({
20      uri: 'http://localhost:3010',
21      credentials: 'same-origin',
22      headers: {
23        cookie: req.header('Cookie'),
24      },
25    }),
26    cache: new InMemoryCache(),
27  });
28
29  const context = {};
30
31  // The client-side App will instead use <BrowserRouter>
32  const App = (
33    <ApolloProvider client={client}>
34      <StaticRouter location={req.url} context={context}>
35        <Layout />
36      </StaticRouter>
37    </ApolloProvider>
38  );
39
40  // TODO: rendering code (see below)
41});
42
43app.listen(basePort, () => console.log(
44  `app Server is now running on http://localhost:${basePort}`
45));

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.

caution
It's important to create an entirely new instance of Apollo Client for each request. Otherwise, your response to a request might include sensitive cached query results from a previous request.

Executing queries with getDataFromTree

Because our app uses Apollo Client, some of the components in the React tree probably execute a GraphQL query with the useQuery hook. We can instruct Apollo Client to execute all of the queries required by the tree's components with the getDataFromTree function.

This function walks down the entire tree and executes every required query it encounters (including nested queries). It returns a Promise that resolves when all result data is ready in the Apollo Client cache.

When the Promise resolves, you're ready to render your React tree and return it, along with the current state of the Apollo Client cache.

Note that if you are rendering your React tree directly to a string (instead of the component-based example below), you will need to use renderToStringWithData instead of getDataFromTree. This will ensure the client-side React hydration works correctly by using ReactDOMServer.renderToString to generate the string.

The following code replaces the TODO comment within the app.use call in the example above:

JavaScript
app.js
1// Add this import to the top of the file
2import { getDataFromTree } from "@apollo/client/react/ssr";
3
4// Replace the TODO with this
5getDataFromTree(App).then((content) => {
6  // Extract the entirety of the Apollo Client cache's current state
7  const initialState = client.extract();
8
9  // Add both the page content and the cache state to a top-level component
10  const html = <Html content={content} state={initialState} />;
11
12  // Render the component to static markup and return it
13  res.status(200);
14  res.send(`<!doctype html>\n${ReactDOM.renderToStaticMarkup(html)}`);
15  res.end();
16});

The definition of the top-level Html component that's rendered to static markup might look like this:

JavaScript
components/html.js
1export function Html({ content, state }) {
2  return (
3    <html>
4      <body>
5        <div id="root" dangerouslySetInnerHTML={{ __html: content }} />
6        <script dangerouslySetInnerHTML={{
7          __html: `window.__APOLLO_STATE__=${JSON.stringify(state).replace(/</g, '\\u003c')};`,
8        }} />
9      </body>
10    </html>
11  );
12}

This results in the rendered React tree being added as a child of the root div, and the initial cache state is assigned to the __APOLLO_STATE__ global object.

The replace call in this example escapes the < character to prevent cross-site scripting attacks that are possible via the presence of </script> in a string literal.

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:

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

JavaScript
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).

One option is to use Apollo Link to fetch data using a local GraphQL schema instead of making a network request. To achieve this, when creating an Apollo Client on the server, you could use a SchemaLink instead of using createHttpLink. SchemaLink uses your schema and context to run the query immediately, without any additional network requests:

JavaScript
1import { ApolloClient, InMemoryCache } from '@apollo/client'
2import { SchemaLink } from '@apollo/client/link/schema';
3
4// ...
5
6const client = new ApolloClient({
7  ssrMode: true,
8  // Instead of "createHttpLink" use SchemaLink here
9  link: new SchemaLink({ schema }),
10  cache: new InMemoryCache(),
11});

Skipping a query

If you want to intentionally skip a particular query during SSR, you can include ssr: false in that query's options. Typically, this means the component is rendered in its "loading" state on the server. For example:

JavaScript
1function withClientOnlyUser() {
2  useQuery(GET_USER_WITH_ID, { ssr: false });
3  return <span>My query won't be run on the server</span>;
4}
Feedback

Edit on GitHub

Forums