From 4ec9f5bafef6fd9bea99f0637a5f92715d7ae161 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 24 Apr 2024 14:12:00 +0200 Subject: [PATCH 1/8] wip --- docs/source/_redirects | 2 ++ docs/source/config.json | 9 +++++++- docs/source/ssr-and-rsc/introduction.mdx | 21 +++++++++++++++++++ .../server-side-rendering-string.mdx} | 0 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 docs/source/ssr-and-rsc/introduction.mdx rename docs/source/{performance/server-side-rendering.mdx => ssr-and-rsc/server-side-rendering-string.mdx} (100%) diff --git a/docs/source/_redirects b/docs/source/_redirects index 689c408a089..80a68d8fd52 100644 --- a/docs/source/_redirects +++ b/docs/source/_redirects @@ -1,3 +1,5 @@ +/docs/react/performance/server-side-rendering /docs/react/ssr-and-rsc/server-side-rendering-string + # Redirect all 3.0 beta docs to root /v3.0-beta/* /docs/react/:splat diff --git a/docs/source/config.json b/docs/source/config.json index 98c46b99f90..bcf62a9d297 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -52,10 +52,17 @@ "Mocking schema capabilities": "/development-testing/client-schema-mocking", "Reducing bundle size": "/development-testing/reducing-bundle-size" }, + "Server-Side Rendering and React Server Components": { + "Introduction": "/ssr-and-rsc/introduction", + "Usage in React Server Components": "", + "Usage in React Client Components with streaming SSR": "", + "Setting up with Next.js": "", + "Setting up a custon \"streaming SSR\" server": "", + "Classic Server-side rendering with `renderToString`": "/ssr-and-rsc/server-side-rendering-string", + }, "Performance": { "Improving performance": "/performance/performance", "Optimistic mutation results": "/performance/optimistic-ui", - "Server-side rendering": "/performance/server-side-rendering", "Compiling queries with Babel": "/performance/babel" }, "Integrations": { diff --git a/docs/source/ssr-and-rsc/introduction.mdx b/docs/source/ssr-and-rsc/introduction.mdx new file mode 100644 index 00000000000..0b83c706bf5 --- /dev/null +++ b/docs/source/ssr-and-rsc/introduction.mdx @@ -0,0 +1,21 @@ +--- +title: Server-Side Rendering and React Server Components - Introduction +--- + +# Disambiguation of essential terms + +Talking about Server-Side Rendering (SSR) and React Server Components (RSC), it's important to understand the distinction between classic SSR and the modern approaches used in frameworks like Next.js. + +* **Classic SSR**, typically executed using React's `renderToString`, involves rendering the entire React component tree on the server into a static HTML string, which is then sent to the browser. +This HTML will be generated in one final pass after all data dependencies have been resolved, which can take a significant amount of time. +Only after that can that HTML be transported to the Browser, where a hydration pass has to happen before the page becomes interactive, often leading to a delay before interactive elements become functional. +This approach is explained in the "Classic Server-side rendering with `renderToString`" section. + +* Modern **streaming SSR** utilizes React Suspense with the `renderToReadableStream` and `renderToPipeableStream` APIs, which support streaming HTML to the browser as soon as suspendse boundaries are ready. +This approach is more efficient, improving Time to Interactive (TTI) by allowing users to see and interact with content as it streams in rather than waiting for the entire bundle. +When the term **SSR** is used outside the "Classic SSR" section, it refers to streaming SSR of React "Client Components". + +* React Server Components (**RSC**) describe an - also streamed - render pass that happens before SSR, and only creates static JSX, which will be rendered into static HTML and is not rehydrated with interactive Components in the browser. +A router can replace the RSC contents of a page by re-initializing an RSC render on the RSC server, and replace the static HTML of the page with the new RSC contents, while leaving interactive React Client Components intact. + +* React **Client Components** are the interactive components that React has been known for the longest time of its existence, rendered either in SSR or after hydration directly in the browser. diff --git a/docs/source/performance/server-side-rendering.mdx b/docs/source/ssr-and-rsc/server-side-rendering-string.mdx similarity index 100% rename from docs/source/performance/server-side-rendering.mdx rename to docs/source/ssr-and-rsc/server-side-rendering-string.mdx From a218bb3afc9269a46ff26aa43888c5530dc16e34 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 24 Apr 2024 15:33:44 +0200 Subject: [PATCH 2/8] wording, create more files --- docs/source/config.json | 10 +++++----- docs/source/ssr-and-rsc/custom-streaming-ssr.mdx | 3 +++ docs/source/ssr-and-rsc/introduction.mdx | 8 ++++++-- docs/source/ssr-and-rsc/nextjs.mdx | 3 +++ docs/source/ssr-and-rsc/usage-in-client-components.mdx | 3 +++ docs/source/ssr-and-rsc/usage-in-rsc.mdx | 3 +++ 6 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 docs/source/ssr-and-rsc/custom-streaming-ssr.mdx create mode 100644 docs/source/ssr-and-rsc/nextjs.mdx create mode 100644 docs/source/ssr-and-rsc/usage-in-client-components.mdx create mode 100644 docs/source/ssr-and-rsc/usage-in-rsc.mdx diff --git a/docs/source/config.json b/docs/source/config.json index bcf62a9d297..7b44ded2134 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -54,11 +54,11 @@ }, "Server-Side Rendering and React Server Components": { "Introduction": "/ssr-and-rsc/introduction", - "Usage in React Server Components": "", - "Usage in React Client Components with streaming SSR": "", - "Setting up with Next.js": "", - "Setting up a custon \"streaming SSR\" server": "", - "Classic Server-side rendering with `renderToString`": "/ssr-and-rsc/server-side-rendering-string", + "Usage in React Server Components": "/ssr-and-rsc/usage-in-rsc.mdx", + "Usage in React Client Components with streaming SSR": "/ssr-and-rsc/usage-in-client-components.mdx", + "Setting up with Next.js": "/ssr-and-rsc/nextjs.mdx", + "Setting up a custom \"streaming SSR\" server": "/ssr-and-rsc/custom-streaming-ssr.mdx", + "Classic Server-side rendering with `renderToString`": "/ssr-and-rsc/server-side-rendering-string" }, "Performance": { "Improving performance": "/performance/performance", diff --git a/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx b/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx new file mode 100644 index 00000000000..eeb51d4ef33 --- /dev/null +++ b/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx @@ -0,0 +1,3 @@ +--- +title: Setting up Apollo Client with a custom "streaming SSR" server +--- diff --git a/docs/source/ssr-and-rsc/introduction.mdx b/docs/source/ssr-and-rsc/introduction.mdx index 0b83c706bf5..67ee794e984 100644 --- a/docs/source/ssr-and-rsc/introduction.mdx +++ b/docs/source/ssr-and-rsc/introduction.mdx @@ -4,10 +4,11 @@ title: Server-Side Rendering and React Server Components - Introduction # Disambiguation of essential terms -Talking about Server-Side Rendering (SSR) and React Server Components (RSC), it's important to understand the distinction between classic SSR and the modern approaches used in frameworks like Next.js. +When discussing Server-Side Rendering (SSR) and React Server Components (RSC), it's necessary to understand the distinction between classic SSR and the modern approaches used in frameworks like Next.js. * **Classic SSR**, typically executed using React's `renderToString`, involves rendering the entire React component tree on the server into a static HTML string, which is then sent to the browser. -This HTML will be generated in one final pass after all data dependencies have been resolved, which can take a significant amount of time. +First, your React tree will render one or multiple times to start all network requests your page needs to render successfully. +Once all these network requests have finished, one final render pass will generate the HTML sent to the browser. Only after that can that HTML be transported to the Browser, where a hydration pass has to happen before the page becomes interactive, often leading to a delay before interactive elements become functional. This approach is explained in the "Classic Server-side rendering with `renderToString`" section. @@ -19,3 +20,6 @@ When the term **SSR** is used outside the "Classic SSR" section, it refers to st A router can replace the RSC contents of a page by re-initializing an RSC render on the RSC server, and replace the static HTML of the page with the new RSC contents, while leaving interactive React Client Components intact. * React **Client Components** are the interactive components that React has been known for the longest time of its existence, rendered either in SSR or after hydration directly in the browser. +You can read more on how React "draws the line" between Client and Server Components in the [React documentation](https://react.dev/reference/react/use-client) + +Generally, most custom implementations will use classic SSR, while frameworks like Next.js and Remix might use streaming SSR and RSC. It is possible to use streaming SSR in a manual setup, but at this point, it still requires a lot of setup. Using RSC without a framework is generally not recommended. diff --git a/docs/source/ssr-and-rsc/nextjs.mdx b/docs/source/ssr-and-rsc/nextjs.mdx new file mode 100644 index 00000000000..4ebc737397d --- /dev/null +++ b/docs/source/ssr-and-rsc/nextjs.mdx @@ -0,0 +1,3 @@ +--- +title: Setting up Apollo Client with Next.js +--- diff --git a/docs/source/ssr-and-rsc/usage-in-client-components.mdx b/docs/source/ssr-and-rsc/usage-in-client-components.mdx new file mode 100644 index 00000000000..378171c202d --- /dev/null +++ b/docs/source/ssr-and-rsc/usage-in-client-components.mdx @@ -0,0 +1,3 @@ +--- +title: Using Apollo Client in React Client Components with streaming SSR +--- diff --git a/docs/source/ssr-and-rsc/usage-in-rsc.mdx b/docs/source/ssr-and-rsc/usage-in-rsc.mdx new file mode 100644 index 00000000000..06faa183ff6 --- /dev/null +++ b/docs/source/ssr-and-rsc/usage-in-rsc.mdx @@ -0,0 +1,3 @@ +--- +title: Using Apollo Client in React Server Components +--- From da18ae8871be76342fd60f66e1a888cf12b241eb Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 26 Apr 2024 11:57:22 +0200 Subject: [PATCH 3/8] first draft of "custom streaming SSR setup" --- .../ssr-and-rsc/custom-streaming-ssr.mdx | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) diff --git a/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx b/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx index eeb51d4ef33..10147b91dd8 100644 --- a/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx +++ b/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx @@ -1,3 +1,259 @@ --- title: Setting up Apollo Client with a custom "streaming SSR" server --- + +> This page covers setting up Apollo Client for usage with `renderToPipeableStream` or `renderToReadableStream` in a custom "streaming SSR" server. +> If you use a framework like Next.js or Redwood.js, these instructions will not apply to your situation. +> If you are using Next.js, you can follow the [Next.js guide](/docs/nextjs) instead. +> Redwood is preconfigured to support Apollo Client in their latest experimental releases with RSC support. + +## Why you need a specific setup for Apollo Client with streaming SSR. + +In a setup with streaming SSR, your server will send the HTML response in chunks as it is being generated. +That means your browser might already be hydrating components - including your `ApolloClient` and `InMemoryCache` instances, while the server is still making GraphQL requests and rendering contents. +Apollo Client needs to be set up with a specific transport mechanism that fulfills the following requirements: + +- When a query is executed on the SSR server, the same query should be simulated as "in flight" in the Browser so that components rendering in the Browser will not trigger the same query argsToArgsConfig. +- When a query receives data on the SSR server, that data should be sent to the browser to hydrate the cache and resolve that simulated "in flight" query. +- When a query receives an error on the SSR server, that error should be signaled to the browser as soon as possible so the browser can retry the query before the component renders. (Error details should not be transported from SSR to the Browser to prevent any potential "server-only" secrets from leaking to the Browser.) +- When a hook is rendered on the server, it has to be ensured that the hydration render in the Browser sees the exact same data the server saw to prevent a hydration mismatch. + +As this is a complex setup, Apollo Client provides the `@apollo/client-react-streaming` package to help you set up Apollo Client in a streaming SSR environment. This functionality is not included in the main `@apollo/client` yet - we are still waiting for React to add some APIs that will make a part of this package obsolete and setup a lot simpler. + +## Setting up Apollo Client with a custom "streaming SSR" server - with the example of a Vite.js server + +This guide assumes you already have a working Vite setup with `renderToReadableStream` or `renderToPipeableStream` in place. +Unfortunately, at the time of writing, there is no official template for Vite with streaming SSR available, so the following section is based off of [letientai299/vite-react-ssr-typescript](https://github.com/letientai299/vite-react-ssr-typescript/tree/6b0b98a2947e1b2d8bbfb610da1e53e474395fe2), which is a variation of the [official Vite React SSR template](https://github.com/bluwy/create-vite-extra/tree/master/template-ssr-react) with streaming SSR support. +You can also find a slightly different variant with a full setup in [the Apollo Client integration tests](https://github.com/apollographql/apollo-client-nextjs/tree/main/integration-test/vite-streaming). + +### 1. Install the necessary package + +As explained above, you will need the `@apollo/client-react-streaming` package, so run the following command to install it: + +```bash +npm install @apollo/client-react-streaming @apollo/client +``` + +### 2. Create a `WrappedApolloProvider` + +Create a file called `Transport.tsx` in your `src` folder with the following content: + +```tsx title="src/Transport.tsx" +import { WrapApolloProvider } from "@apollo/client-react-streaming"; +import { buildManualDataTransport } from "@apollo/client-react-streaming/manual-transport"; +import * as React from "react"; + +const InjectionContext = React.createContext< + (callback: () => React.ReactNode) => void +>(() => {}); + +export const InjectionContextProvider = InjectionContext.Provider; + +export const WrappedApolloProvider = WrapApolloProvider( + buildManualDataTransport({ + useInsertHtml() { + return React.useContext(InjectionContext); + }, + }) +); +``` + +Here, you combined a few building blocks: + +- `buildManualDataTransport` this creates a "manual data transport". In the future, there might be other kinds of data transport depending on your setup and React version. +- `WrapApolloProvider` creates a version of an `ApolloProvider` component that is optimized for streaming SSR for a data transport of your choice. It has a different signature in that it doesn't accept `client` prop, but a `makeClient` prop. +- `InjectionContext` is created so you can pass in a custom `injectIntoStream` method when rendering your app on the server. + +### 3. Update your `Html.tsx` file to use `InjectionContextProvider` + +```diff title="src/Html.tsx" ++import { InjectionContextProvider } from "./Transport"; + + interface HtmlProps { + children: ReactNode; ++ injectIntoStream: (callback: () => React.ReactNode) => void; + } + +-function Html({ children }: HtmlProps) { ++function Html({ children, injectIntoStream }: HtmlProps) { + +// ... + + ++ +
{children}
++
+ + + ); +``` + +### 4. Update your `entry-server.tsx` to enable injecting data into the React Stream + +```diff + import type { Request, Response } from "express"; + import App from "./src/App"; + import Html from "./src/Html"; ++import { Writable } from "node:stream"; ++import { ++ createInjectionTransformStream, ++ pipeReaderToResponse, ++} from "@apollo/client-react-streaming/stream-utils"; + + export function render(req: Request, res: Response, bootstrap: string) { ++ const { transformStream, injectIntoStream } = ++ createInjectionTransformStream(); + const { pipe } = ReactDOMServer.renderToPipeableStream( +- ++ + + , + { + onShellReady() { + res.statusCode = 200; + res.setHeader("content-type", "text/html"); +- pipe(res); ++ pipeReaderToResponse(transformStream.readable.getReader(), res); ++ pipe(Writable.fromWeb(transformStream.writable)); + }, + bootstrapModules: [bootstrap], + } +``` + +Alternatively, if you are using `renderToReadableStream`, your new setup might look like this: + +```js +const reactStream = await renderToReadableStream( + + + , + { + /* ...options... */ + } +); + +await pipeReaderToResponse( + reactStream.pipeThrough(transformStream).getReader(), + res +); +``` + +The important parts here are: + +- `createInjectionTransformStream` creates a `transformStream` and a `injectIntoStream` function. +- You forward the `injectIntoStream` function into your `Html` component so that you can use it to inject data into the stream. +- Instead of piping the React stream directly into the Response, you pipe it into the `transformStream` and then pipe the transformed stream into the Response - this varies depending on the streaming API you are using. + +### 5. Create a `makeClient` function + +This is very similar to a typical Apollo Client setup, with the exception of being wrapped in a method. This "wrapping" is necessary so during SSR every client instance is unique and doesn't share any state between requests. + +Create a file called `client.ts` in your `src` folder with the following content: + +```ts title="src/client.ts" +import { ApolloClient, InMemoryCache } from "@apollo/client-react-streaming"; +import { HttpLink } from "@apollo/client"; + +export const makeClient = () => { + const link = new HttpLink({ + uri: "https://flyby-router-demo.herokuapp.com/'", + }); + return new ApolloClient({ + link, + cache: new InMemoryCache(), + }); +}; +``` + +Important bits here : + +- `ApolloClient` and `InMemoryCache` are imported from `@apollo/client-react-streaming` instead of `@apollo/client`. +- instead of exporting a `client` variable, you export a `makeClient` function that creates a new client instance every time it is called. + +### 6. Update your `App.tsx` to use `WrappedApolloProvider` and `makeClient` + +```diff + import "./App.css"; ++import { WrappedApolloProvider } from "./Transport"; ++import { makeClient } from "./client"; + + function App() { + return ( ++ +
+ // ... +
++
+ ); + } +``` + +### 7. Start using suspense-enabled hooks in your application + +At this point, you're all set. + +You can now use the suspense-enabled hooks `useSuspenseQuery` and `useBackgroundQuery`/`useReadQuery` in your application, and their data will be streamed from the server to the browser as it comes in. + +Give it a try - create a component that uses `useSuspenseQuery`: + +```ts title="src/DisplayLocations.js" +import { gql, useSuspenseQuery } from "@apollo/client"; +import type { TypedDocumentNode } from "@apollo/client"; + +const GET_LOCATIONS: TypedDocumentNode<{ + locations: Array<{ + id: string; + name: string; + description: string; + photo: string; + }>; +}> = gql` + query GetLocations { + locations { + id + name + description + photo + } + } +`; + +export function DisplayLocations() { + const { data } = useSuspenseQuery(GET_LOCATIONS); + console.log(data); + return data.locations.map(({ id, name, description, photo }) => ( +
+

{name}

+ location-reference +
+ About this location: +

{description}

+
+
+ )); +} +``` + +and hook it up: + +```diff title="src/App.tsx" +-import { useState } from "react"; ++import { Suspense, useState } from "react"; ++import { DisplayLocations } from "./DisplayLocations"; + +// ... + +

Vite + React + TS + SSR

+ ++ ++ ++ +

+``` + +If you open this page in your browser, you should see a "loading" message, which will be replaced by the actual data as soon as it arrives from the server. + +Take a look at your Devtools' Network tab: you will notice that there is no GraphQL request happening in the browser. +The request was made during SSR, and the result has been streamed over. + +After hydration, your page is fully working "in the browser", so any future requests after the initial render will be made from the Browser. From 59d6bde8257853da25c4e56ea6074a6a355c021e Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 26 Apr 2024 12:49:40 +0200 Subject: [PATCH 4/8] fixup --- docs/source/config.json | 8 ++++---- docs/source/ssr-and-rsc/custom-streaming-ssr.mdx | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/source/config.json b/docs/source/config.json index 1a94f842810..698a52d4122 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -55,10 +55,10 @@ }, "Server-Side Rendering and React Server Components": { "Introduction": "/ssr-and-rsc/introduction", - "Usage in React Server Components": "/ssr-and-rsc/usage-in-rsc.mdx", - "Usage in React Client Components with streaming SSR": "/ssr-and-rsc/usage-in-client-components.mdx", - "Setting up with Next.js": "/ssr-and-rsc/nextjs.mdx", - "Setting up a custom \"streaming SSR\" server": "/ssr-and-rsc/custom-streaming-ssr.mdx", + "Usage in React Server Components": "/ssr-and-rsc/usage-in-rsc", + "Usage in React Client Components with streaming SSR": "/ssr-and-rsc/usage-in-client-components", + "Setting up with Next.js": "/ssr-and-rsc/nextjs", + "Setting up a custom \"streaming SSR\" server": "/ssr-and-rsc/custom-streaming-ssr", "Classic Server-side rendering with `renderToString`": "/ssr-and-rsc/server-side-rendering-string" }, "Performance": { diff --git a/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx b/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx index 10147b91dd8..a5716549fab 100644 --- a/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx +++ b/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx @@ -193,6 +193,7 @@ Important bits here : At this point, you're all set. You can now use the suspense-enabled hooks `useSuspenseQuery` and `useBackgroundQuery`/`useReadQuery` in your application, and their data will be streamed from the server to the browser as it comes in. +For more details on these hooks, check out the [Apollo Client React Suspense documentation](../data/suspense). Give it a try - create a component that uses `useSuspenseQuery`: From ff0ad8e8c380a0400f74ecfda74c109cdccc5a69 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 26 Apr 2024 12:51:13 +0200 Subject: [PATCH 5/8] more fixup --- docs/source/ssr-and-rsc/custom-streaming-ssr.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx b/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx index a5716549fab..104701ea117 100644 --- a/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx +++ b/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx @@ -156,7 +156,7 @@ import { HttpLink } from "@apollo/client"; export const makeClient = () => { const link = new HttpLink({ - uri: "https://flyby-router-demo.herokuapp.com/'", + uri: "https://flyby-router-demo.herokuapp.com/", }); return new ApolloClient({ link, From 2ec3813aa1dcc75dd14ed25516293dc9576d2122 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 26 Apr 2024 13:07:08 +0200 Subject: [PATCH 6/8] small tweaks and formatting --- .prettierignore | 3 + .../ssr-and-rsc/custom-streaming-ssr.mdx | 14 ++-- docs/source/ssr-and-rsc/introduction.mdx | 24 +++---- .../server-side-rendering-string.mdx | 68 +++++++++---------- 4 files changed, 59 insertions(+), 50 deletions(-) diff --git a/.prettierignore b/.prettierignore index 4af59c9d031..ec64aebf9dc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -25,6 +25,9 @@ /docs/source/development-testing/** !/docs/source/development-testing/reducing-bundle-size.mdx !/docs/source/development-testing/schema-driven-testing.mdx +!/docs/source/ssr-and-rsc +!/docs/source/ssr-and-rsc/** + !docs/shared /docs/shared/** diff --git a/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx b/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx index 104701ea117..450c28a0d2e 100644 --- a/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx +++ b/docs/source/ssr-and-rsc/custom-streaming-ssr.mdx @@ -2,10 +2,16 @@ title: Setting up Apollo Client with a custom "streaming SSR" server --- -> This page covers setting up Apollo Client for usage with `renderToPipeableStream` or `renderToReadableStream` in a custom "streaming SSR" server. -> If you use a framework like Next.js or Redwood.js, these instructions will not apply to your situation. -> If you are using Next.js, you can follow the [Next.js guide](/docs/nextjs) instead. -> Redwood is preconfigured to support Apollo Client in their latest experimental releases with RSC support. + + +This page covers setting up Apollo Client for usage with `renderToPipeableStream` or `renderToReadableStream` in a custom "streaming SSR" server. + +If you use a framework like Next.js or Redwood.js, these instructions will not apply to your situation: + +- If you are using Next.js, please follow the [Next.js guide](/docs/nextjs) instead. +- Redwood is preconfigured to support Apollo Client in their latest experimental releases with RSC support, so no additional setup is required. + + ## Why you need a specific setup for Apollo Client with streaming SSR. diff --git a/docs/source/ssr-and-rsc/introduction.mdx b/docs/source/ssr-and-rsc/introduction.mdx index 67ee794e984..0d3f6395374 100644 --- a/docs/source/ssr-and-rsc/introduction.mdx +++ b/docs/source/ssr-and-rsc/introduction.mdx @@ -6,20 +6,20 @@ title: Server-Side Rendering and React Server Components - Introduction When discussing Server-Side Rendering (SSR) and React Server Components (RSC), it's necessary to understand the distinction between classic SSR and the modern approaches used in frameworks like Next.js. -* **Classic SSR**, typically executed using React's `renderToString`, involves rendering the entire React component tree on the server into a static HTML string, which is then sent to the browser. -First, your React tree will render one or multiple times to start all network requests your page needs to render successfully. -Once all these network requests have finished, one final render pass will generate the HTML sent to the browser. -Only after that can that HTML be transported to the Browser, where a hydration pass has to happen before the page becomes interactive, often leading to a delay before interactive elements become functional. -This approach is explained in the "Classic Server-side rendering with `renderToString`" section. +- **Classic SSR**, typically executed using React's `renderToString`, involves rendering the entire React component tree on the server into a static HTML string, which is then sent to the browser. + First, your React tree will render one or multiple times to start all network requests your page needs to render successfully. + Once all these network requests have finished, one final render pass will generate the HTML sent to the browser. + Only after that can that HTML be transported to the Browser, where a hydration pass has to happen before the page becomes interactive, often leading to a delay before interactive elements become functional. + This approach is explained in the "Classic Server-side rendering with `renderToString`" section. -* Modern **streaming SSR** utilizes React Suspense with the `renderToReadableStream` and `renderToPipeableStream` APIs, which support streaming HTML to the browser as soon as suspendse boundaries are ready. -This approach is more efficient, improving Time to Interactive (TTI) by allowing users to see and interact with content as it streams in rather than waiting for the entire bundle. -When the term **SSR** is used outside the "Classic SSR" section, it refers to streaming SSR of React "Client Components". +- Modern **streaming SSR** utilizes React Suspense with the `renderToReadableStream` and `renderToPipeableStream` APIs, which support streaming HTML to the browser as soon as suspendse boundaries are ready. + This approach is more efficient, improving Time to Interactive (TTI) by allowing users to see and interact with content as it streams in rather than waiting for the entire bundle. + When the term **SSR** is used outside the "Classic SSR" section, it refers to streaming SSR of React "Client Components". -* React Server Components (**RSC**) describe an - also streamed - render pass that happens before SSR, and only creates static JSX, which will be rendered into static HTML and is not rehydrated with interactive Components in the browser. -A router can replace the RSC contents of a page by re-initializing an RSC render on the RSC server, and replace the static HTML of the page with the new RSC contents, while leaving interactive React Client Components intact. +- React Server Components (**RSC**) describe an - also streamed - render pass that happens before SSR, and only creates static JSX, which will be rendered into static HTML and is not rehydrated with interactive Components in the browser. + A router can replace the RSC contents of a page by re-initializing an RSC render on the RSC server, and replace the static HTML of the page with the new RSC contents, while leaving interactive React Client Components intact. -* React **Client Components** are the interactive components that React has been known for the longest time of its existence, rendered either in SSR or after hydration directly in the browser. -You can read more on how React "draws the line" between Client and Server Components in the [React documentation](https://react.dev/reference/react/use-client) +- React **Client Components** are the interactive components that React has been known for the longest time of its existence, rendered either in SSR or after hydration directly in the browser. + You can read more on how React "draws the line" between Client and Server Components in the [React documentation](https://react.dev/reference/react/use-client) Generally, most custom implementations will use classic SSR, while frameworks like Next.js and Remix might use streaming SSR and RSC. It is possible to use streaming SSR in a manual setup, but at this point, it still requires a lot of setup. Using RSC without a framework is generally not recommended. diff --git a/docs/source/ssr-and-rsc/server-side-rendering-string.mdx b/docs/source/ssr-and-rsc/server-side-rendering-string.mdx index e6e0e9ecda3..fb04cbeccd7 100644 --- a/docs/source/ssr-and-rsc/server-side-rendering-string.mdx +++ b/docs/source/ssr-and-rsc/server-side-rendering-string.mdx @@ -22,32 +22,28 @@ Apollo Client provides a handy API for using it with server-side rendering, incl 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](https://reactrouter.com/web/guides/server-rendering). +- You need to use a server-compatible router for React, such as [React Router](https://reactrouter.com/web/guides/server-rendering). - (In the case of React Router, you wrap your application in a `StaticRouter` component instead of the `BrowserRouter` you use on the client side.) + (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. +- You need to replace relative URLs with absolute URLs wherever applicable. -* The initialization of Apollo Client changes slightly, as [described below](#initializing-apollo-client). +- The initialization of Apollo Client changes slightly, as [described below](#initializing-apollo-client). ## Initializing Apollo Client Here's an example _server-side_ initialization of Apollo Client: ```js {7-17} -import { - ApolloClient, - createHttpLink, - InMemoryCache -} from '@apollo/client'; +import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client"; const client = new ApolloClient({ ssrMode: true, link: createHttpLink({ - uri: 'http://localhost:3010', - credentials: 'same-origin', + uri: "http://localhost:3010", + credentials: "same-origin", headers: { - cookie: req.header('Cookie'), + cookie: req.header("Cookie"), }, }), cache: new InMemoryCache(), @@ -56,11 +52,11 @@ const client = new ApolloClient({ 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). +- 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. +- 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). + 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](#avoiding-the-network-for-local-queries). @@ -77,25 +73,24 @@ import { ApolloProvider, ApolloClient, createHttpLink, - InMemoryCache -} from '@apollo/client'; -import Express from 'express'; -import React from 'react'; -import { StaticRouter } from 'react-router'; + InMemoryCache, +} from "@apollo/client"; +import Express from "express"; +import React from "react"; +import { StaticRouter } from "react-router"; // File shown below -import Layout from './routes/Layout'; +import Layout from "./routes/Layout"; const app = new Express(); app.use((req, res) => { - const client = new ApolloClient({ ssrMode: true, link: createHttpLink({ - uri: 'http://localhost:3010', - credentials: 'same-origin', + uri: "http://localhost:3010", + credentials: "same-origin", headers: { - cookie: req.header('Cookie'), + cookie: req.header("Cookie"), }, }), cache: new InMemoryCache(), @@ -115,9 +110,9 @@ app.use((req, res) => { // TODO: rendering code (see below) }); -app.listen(basePort, () => console.log( - `app Server is now running on http://localhost:${basePort}` -)); +app.listen(basePort, () => + console.log(`app Server is now running on http://localhost:${basePort}`) +); ``` @@ -165,9 +160,14 @@ export function Html({ content, state }) {

-