Skip to content

Commit f61aaee

Browse files
update SSR documentation to show prerenderStatic and link to integration packages (#12958)
* update SSR documentation to show `prerenderStatic` and link to integration packages * Apply suggestions from code review Co-authored-by: Jerel Miller <[email protected]> * Apply suggestions from code review * Update docs/source/performance/server-side-rendering.mdx * prettier * bold -> h5 --------- Co-authored-by: Jerel Miller <[email protected]>
1 parent 1ae6834 commit f61aaee

File tree

1 file changed

+218
-44
lines changed

1 file changed

+218
-44
lines changed

docs/source/performance/server-side-rendering.mdx

Lines changed: 218 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
title: Server-side rendering
33
---
44

5+
<Note>
6+
This page describes manual server side rendering setups with React. If you are using a modern SSR-focused framework like [Next.js](https://nextjs.org/), [React Router Framework mode](https://reactrouter.com/en/main/routers/framework), or [TanStack Start](https://start.tanstack.com/), you need to use one of our Apollo Client Framework integrations instead.
7+
You can find these integrations [on GitHub](https://github.com/apollographql/apollo-client-integrations):
8+
9+
- [@apollo/client-integration-nextjs](https://github.com/apollographql/apollo-client-integrations/tree/main/packages/nextjs)
10+
- [@apollo/client-integration-react-router](https://github.com/apollographql/apollo-client-integrations/tree/main/packages/react-router)
11+
- [@apollo/client-integration-tanstack-start](https://github.com/apollographql/apollo-client-integrations/tree/main/packages/tanstack-start)
12+
13+
</Note>
14+
515
**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:
616

717
```mermaid
@@ -34,12 +44,12 @@ When you render your React app on the server side, _most_ of the code is identic
3444

3545
Here's an example _server-side_ initialization of Apollo Client:
3646

37-
```js {7-17}
38-
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
47+
```js
48+
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
3949

4050
const client = new ApolloClient({
4151
ssrMode: true,
42-
link: createHttpLink({
52+
link: new HttpLink({
4353
uri: "http://localhost:3010",
4454
credentials: "same-origin",
4555
headers: {
@@ -50,13 +60,9 @@ const client = new ApolloClient({
5060
});
5161
```
5262

53-
You'll notice a couple differences from a typical client-side initialization:
54-
55-
- You provide `ssrMode: true`. This prevents Apollo Client from refetching queries unnecessarily, and it also enables you to use the `getDataFromTree` function (covered below).
63+
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.
5664

57-
- 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.
58-
59-
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).
65+
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.
6066

6167
> 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](#avoiding-the-network-for-local-queries).
6268
@@ -69,7 +75,7 @@ First, here's an example `app.js` file, _without_ the code for rendering React t
6975
<ExpansionPanel title="Click to expand">
7076

7177
```jsx title="app.js"
72-
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
78+
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
7379
import { ApolloProvider } from "@apollo/client/react";
7480
import Express from "express";
7581
import React from "react";
@@ -82,7 +88,7 @@ const app = new Express();
8288
app.use((req, res) => {
8389
const client = new ApolloClient({
8490
ssrMode: true,
85-
link: createHttpLink({
91+
link: new HttpLink({
8692
uri: "http://localhost:3010",
8793
credentials: "same-origin",
8894
headers: {
@@ -121,62 +127,209 @@ It's important to create an _entirely new instance_ of Apollo Client for each re
121127

122128
</Caution>
123129

124-
### Executing queries with `getDataFromTree`
130+
### Executing queries with `prerenderStatic`
131+
132+
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.
125133

126-
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.
134+
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.
127135

128-
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.
136+
The function returns a `Promise` that resolves when all result data is ready in the Apollo Client cache and the final render is complete.
129137

130-
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.
138+
#### Choosing a rendering function
131139

132-
> 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`](../api/react/ssr/#rendertostringwithdata) instead of `getDataFromTree`. This will ensure the client-side React hydration works correctly by using [`ReactDOMServer.renderToString`](https://react.dev/reference/react-dom/server/renderToString) to generate the string.
140+
`prerenderStatic` supports multiple React rendering functions:
141+
142+
- **`prerender`** from `react-dom/static` - Recommended for Deno or modern edge runtimes with Web Streams. Supports React Suspense.
143+
- **`prerenderToNodeStream`** from `react-dom/static` - Recommended for Node.js. Supports React Suspense.
144+
- **`renderToString`** from `react-dom/server` - Legacy API without Suspense support. Won't work with suspenseful hooks.
145+
- **`renderToStaticMarkup`** from `react-dom/server` - Legacy API without Suspense support. Slightly faster than `renderToString`, but the result cannot be hydrated.
133146

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

136149
```js title="app.js"
137-
// Add this import to the top of the file
138-
import { getDataFromTree } from "@apollo/client/react/ssr";
150+
// Add these imports to the top of the file
151+
import { prerenderStatic } from "@apollo/client/react/ssr";
152+
import { prerenderToNodeStream } from "react-dom/static";
139153

140154
// Replace the TODO with this
141-
getDataFromTree(App).then((content) => {
155+
prerenderStatic({
156+
tree: App,
157+
// this is optional if your `App` component contains an <ApolloProvider>
158+
context: { client },
159+
renderFunction: prerenderToNodeStream,
160+
}).then(async ({ result }) => {
142161
// Extract the entirety of the Apollo Client cache's current state
143162
const initialState = client.extract();
144163

145-
// Add both the page content and the cache state to a top-level component
146-
const html = <Html content={content} state={initialState} />;
164+
// TODO: Send the response to the client (see below for examples)
165+
});
166+
```
167+
168+
#### Sending the response
147169

148-
// Render the component to static markup and return it
149-
res.status(200);
150-
res.send(`<!doctype html>\n${ReactDOM.renderToStaticMarkup(html)}`);
151-
res.end();
170+
After `prerenderStatic` completes, you need to send the HTML response to the client. The approach depends on the rendering function you chose.
171+
172+
##### Using streaming with `renderToPipeableStream`
173+
174+
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:
175+
176+
```js title="app.js"
177+
import { renderToPipeableStream } from "react-dom/server";
178+
179+
// After prerenderStatic completes
180+
prerenderStatic({
181+
tree: App,
182+
context: { client },
183+
renderFunction: prerenderToNodeStream,
184+
}).then(async ({ result }) => {
185+
const initialState = client.extract();
186+
187+
// Render the app again with streaming, injecting the Apollo state
188+
const { pipe } = renderToPipeableStream(
189+
<html>
190+
<head>
191+
<title>My App</title>
192+
</head>
193+
<body>
194+
<div id="root">
195+
<App />
196+
</div>
197+
</body>
198+
</html>,
199+
{
200+
bootstrapScriptContent: `window.__APOLLO_STATE__=${JSON.stringify(
201+
initialState
202+
).replace(/</g, "\\u003c")}`,
203+
bootstrapScripts: ["/client.js"],
204+
onShellReady() {
205+
// Start streaming the reponse to the browser
206+
res.setHeader("Content-Type", "text/html");
207+
res.statusCode = 200;
208+
pipe(res);
209+
},
210+
onError(error) {
211+
console.error("Rendering error:", error);
212+
},
213+
}
214+
);
152215
});
153216
```
154217

155-
The definition of the top-level `Html` component that's rendered to static markup might look like this:
218+
<Note>
219+
Instead of rendering `<div id="root"><App /></div>`, you can also render `<div id="root" dangerouslySetInnerHTML={{ __html: result }} />` to avoid rendering the entire tree twice.
220+
However, if you go this approach, React cannot decide on the order in which components are streamed to the browser, which might be suboptimal.
221+
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.
222+
</Note>
223+
224+
<Tip>
225+
If you are only using suspenseful hooks (like `useSuspenseQuery` or `useBackgroundQuery`) and no `useQuery` hooks, is also possible to use the [@apollo/client-react-streaming](https://github.com/apollographql/apollo-client-integrations/tree/main/packages/client-react-streaming) package to stream data dynamically while it is fetching, without requiring a first render pass with `prerenderStatic`.
226+
You can find an example application that uses this approach in the [integration tests for that package](https://github.com/apollographql/apollo-client-integrations/tree/main/integration-test/vite-streaming).
227+
</Tip>
228+
229+
<Note>
230+
The `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.
231+
</Note>
156232

157-
```jsx title="components/html.js"
158-
export function Html({ content, state }) {
159-
return (
233+
##### Using `renderToString` for synchronous rendering
234+
235+
For simpler use cases or environments without stream support, you can use `renderToString` to render the entire page synchronously:
236+
237+
```js title="app.js"
238+
import { renderToString } from "react-dom/server";
239+
240+
// After prerenderStatic completes
241+
prerenderStatic({
242+
tree: App,
243+
context: { client },
244+
renderFunction: renderToString,
245+
}).then(async ({ result }) => {
246+
const initialState = client.extract();
247+
248+
// Create a complete HTML document with the cache state
249+
const Html = () => (
160250
<html>
251+
<head>
252+
<title>My App</title>
253+
</head>
161254
<body>
162-
<div id="root" dangerouslySetInnerHTML={{ __html: content }} />
255+
<div id="root" dangerouslySetInnerHTML={{ __html: result }} />
163256
<script
164257
dangerouslySetInnerHTML={{
165-
__html: `window.__APOLLO_STATE__=${JSON.stringify(state).replace(
166-
/</g,
167-
"\\u003c"
168-
)};`,
258+
__html: `window.__APOLLO_STATE__=${JSON.stringify(
259+
initialState
260+
).replace(/</g, "\\u003c")};`,
169261
}}
170262
/>
263+
<script src="/client.js" />
171264
</body>
172265
</html>
173266
);
267+
268+
// Render to string and send
269+
const html = renderToString(<Html />);
270+
271+
res.setHeader("Content-Type", "text/html");
272+
res.status(200);
273+
res.send(`<!DOCTYPE html>${html}`);
274+
res.end();
275+
});
276+
```
277+
278+
<Note>
279+
The `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.
280+
</Note>
281+
282+
#### Advanced options
283+
284+
`prerenderStatic` provides several options to customize the rendering process:
285+
286+
##### Diagnostics
287+
288+
You can enable diagnostics to detect inefficient rendering structures (like `useQuery` waterfalls) in your app:
289+
290+
```js
291+
const { diagnostics } = await prerenderStatic({
292+
tree: App,
293+
context: { client },
294+
renderFunction: prerenderToNodeStream,
295+
diagnostics: true,
296+
});
297+
298+
console.log(`Rendered ${diagnostics.renderCount} times`);
299+
// If renderCount is high, consider using fragment colocation
300+
```
301+
302+
**Timeout support**
303+
304+
You can use an `AbortSignal` to stop the render loop early:
305+
306+
```js
307+
const signal = AbortSignal.timeout(2000); // 2 second timeout
308+
309+
const { result, aborted } = await prerenderStatic({
310+
tree: App,
311+
context: { client },
312+
renderFunction: (tree) => prerenderToNodeStream(tree, { signal }),
313+
signal,
314+
});
315+
316+
if (aborted) {
317+
console.log("Render timed out, returning partial result");
174318
}
175319
```
176320

177-
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.
321+
##### Maximum rerenders
178322

179-
> 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.
323+
If you have deep `useQuery` waterfalls, you can increase the `maxRerenders` option (default: 50):
324+
325+
```js
326+
await prerenderStatic({
327+
tree: App,
328+
context: { client },
329+
renderFunction: prerenderToNodeStream,
330+
maxRerenders: 100,
331+
});
332+
```
180333

181334
### Rehydrating the client-side cache
182335

@@ -209,7 +362,7 @@ const client = new ApolloClient({
209362

210363
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).
211364

212-
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](../api/link/apollo-link-schema/) instead of using `createHttpLink`. `SchemaLink` uses your schema and context to run the query immediately, without any additional network requests:
365+
When creating an Apollo Client on the server, use a [SchemaLink](../api/link/apollo-link-schema/) instead of an `HttpLink`. `SchemaLink` uses your schema and context to run the query immediately, without the need for a network request:
213366

214367
```js
215368
import { ApolloClient, InMemoryCache } from "@apollo/client";
@@ -218,20 +371,41 @@ import { SchemaLink } from "@apollo/client/link/schema";
218371
// ...
219372

220373
const client = new ApolloClient({
221-
ssrMode: true,
222-
// Instead of "createHttpLink" use SchemaLink here
374+
// Instead of HttpLink use SchemaLink here
223375
link: new SchemaLink({ schema }),
224376
cache: new InMemoryCache(),
225377
});
226378
```
227379

228-
## Skipping a query
380+
## Selectively disabling query execution during SSR
229381

230-
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:
382+
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.
383+
384+
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.
231385

232386
```jsx
233-
function withClientOnlyUser() {
234-
useQuery(GET_USER_WITH_ID, { ssr: false });
235-
return <span>My query won't be run on the server</span>;
387+
function ClientOnlyUser() {
388+
const { loading, data } = useQuery(GET_USER_WITH_ID, { ssr: false });
389+
390+
if (loading) {
391+
return <span>Loading...</span>;
392+
}
393+
394+
return <span>User: {data?.user?.name || "Not loaded"}</span>;
236395
}
237396
```
397+
398+
<Note>
399+
400+
`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`.
401+
402+
</Note>
403+
404+
## Legacy APIs
405+
406+
Apollo Client provides two legacy SSR functions that are both replaced by `prerenderStatic`:
407+
408+
- **`getDataFromTree`** - Executes all queries in a component tree and returns when data is ready. Uses `renderToStaticMarkup` by default.
409+
- **`renderToStringWithData`** - Similar to `getDataFromTree`, but returns the rendered string using `renderToString`.
410+
411+
These functions are deprecated. Use `prerenderStatic` instead, which offers better flexibility and performance with modern React rendering APIs.

0 commit comments

Comments
 (0)