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 theBrowserRouter
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:
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 thegetDataFromTree
function (covered below).Instead of providing a
uri
option, you provide anHttpLink
instance to thelink
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
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.
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 ofgetDataFromTree
. This will ensure the client-side React hydration works correctly by usingReactDOMServer.renderToString
to generate the string.
The following code replaces the TODO
comment within the app.use
call in the example above:
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:
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 extract
ed 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).
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:
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:
1function withClientOnlyUser() {
2 useQuery(GET_USER_WITH_ID, { ssr: false });
3 return <span>My query won't be run on the server</span>;
4}