From f77b46ac3178cf09a8b4cdb8c8db96b042b7b08d Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 11 Aug 2021 19:10:53 +0100 Subject: [PATCH] (core) - Add staleWhileRevalidate option to ssrExchange (#1852) * (core) - Add staleWhileRevalidate to ssrExchange * Add staleWhileRevalidate to next-urql * (docs) - document ssrExchange/next-urql staleWhileRevalidate option (#1853) * add docs about staleWhileRevalidate * Apply suggestions from code review Co-authored-by: Phil Pluckthun * Apply suggestions from code review Co-authored-by: Phil Pluckthun Co-authored-by: Phil Pluckthun Co-authored-by: Jovi De Croock --- .changeset/new-horses-bow.md | 5 +++++ .changeset/spicy-poems-rule.md | 5 +++++ docs/advanced/server-side-rendering.md | 22 +++++++++++++++++++++- docs/api/core.md | 8 +++++--- packages/core/src/exchanges/cache.ts | 2 +- packages/core/src/exchanges/ssr.ts | 11 ++++++++++- packages/next-urql/src/types.ts | 1 + packages/next-urql/src/with-urql-client.ts | 6 +++++- 8 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 .changeset/new-horses-bow.md create mode 100644 .changeset/spicy-poems-rule.md diff --git a/.changeset/new-horses-bow.md b/.changeset/new-horses-bow.md new file mode 100644 index 0000000000..e7761d0b2c --- /dev/null +++ b/.changeset/new-horses-bow.md @@ -0,0 +1,5 @@ +--- +'next-urql': minor +--- + +Add new `staleWhileRevalidate` option from the `ssrExchange` addition to `withUrqlClient`'s options. This is useful when Next.js is used in static site generation (SSG) mode. diff --git a/.changeset/spicy-poems-rule.md b/.changeset/spicy-poems-rule.md new file mode 100644 index 0000000000..ce9005e030 --- /dev/null +++ b/.changeset/spicy-poems-rule.md @@ -0,0 +1,5 @@ +--- +'@urql/core': minor +--- + +Add a `staleWhileRevalidate` option to the `ssrExchange`, which allows the client to immediately refetch a new result on hydration, which may be used for cached / stale SSR or SSG pages. This is different from using `cache-and-network` by default (which isn't recommended) as the `ssrExchange` typically acts like a "replacement fetch request". diff --git a/docs/advanced/server-side-rendering.md b/docs/advanced/server-side-rendering.md index fdcf14b3dd..c92c376092 100644 --- a/docs/advanced/server-side-rendering.md +++ b/docs/advanced/server-side-rendering.md @@ -41,6 +41,9 @@ const client = createClient({ The `ssrExchange` must be initialized with the `isClient` and `initialState` options. The `isClient` option tells the exchange whether it's on the server- or client-side. In our example we use `typeof window` to determine this, but in Webpack environments you may also be able to use `process.browser`. +Optionally, we may also choose to enable `staleWhileRevalidate`. When enabled this flag will ensure that although a result may have been rehydrated from our SSR result, another +refetch `network-only` operation will be issued, to update stale data. This is useful for statically generated sites (SSG) that may ship stale data to our application initially. + The `initialState` option should be set to the serialized data you retrieve on your server-side. This data may be retrieved using methods on `ssrExchange()`. You can retrieve the serialized data after server-side rendering using `ssr.extractData()`: @@ -204,7 +207,7 @@ Optimization"](https://nextjs.org/docs/advanced-features/automatic-static-optimi // pages/index.js import React from 'react'; import Head from 'next/head'; -import { useQuery } from "urql"; +import { useQuery } from 'urql'; import { withUrqlClient } from 'next-urql'; const Index = () => { @@ -305,6 +308,23 @@ The above example will make sure the page is rendered as a static-page, it's imp so in our case we were only interested in getting our todos, if there are child components relying on data you'll have to make sure these are fetched as well. +### Stale While Revalidate + +If we choose to use Next's static site generation (SSG or ISG) we may be embedding data in our initial payload that's stale on the client. In this case, we may want to update this data immediately after rehydration. +We can pass `staleWhileRevalidate: true` to `withUrqlClient`'s second option argument to Switch it to a mode where it'll refresh its rehydrated data immediately by issuing another network request. + +```js +export default withUrqlClient( + ssr => ({ + url: 'your-url', + }), + { staleWhileRevalidate: true } +)(...); +``` + +Now, although on rehydration we'll receive the stale data from our `ssrExchange` first, it'll also immediately issue another `network-only` operation to update the data. +During this revalidation our stale results will be marked using `result.stale`. While this is similar to what we see with `cache-and-network` without server-side rendering, it isn't quite the same. Changing the request policy wouldn't actually refetch our data on rehydration as the `ssrExchange` is simply a replacement of a full network request. Hence, this flag allows us to treat this case separately. + ### Resetting the client instance In rare scenario's you possibly will have to reset the client instance (reset all cache, ...), this diff --git a/docs/api/core.md b/docs/api/core.md index de3156ea31..d81d221763 100644 --- a/docs/api/core.md +++ b/docs/api/core.md @@ -280,11 +280,13 @@ The `ssrExchange` as [described on the "Server-side Rendering" page.](../advanced/server-side-rendering.md). It's of type `Options => Exchange`. -It accepts two inputs, `initialState` which is completely +It accepts three inputs, `initialState` which is completely optional and populates the server-side rendered data with -a rehydrated cache, and `isClient` which can be set to +a rehydrated cache, `isClient` which can be set to `true` or `false` to tell the `ssrExchange` whether to -write to (server-side) or read from (client-side) the cache. +write to (server-side) or read from (client-side) the cache, and +`staleWhileRevalidate` which will treat rehydrated data as stale +and refetch up-to-date data by reexecuring the operation using a `network-only` requests policy. By default, `isClient` defaults to `true` when the `Client.suspense` mode is disabled and to `false` when the `Client.suspense` mode diff --git a/packages/core/src/exchanges/cache.ts b/packages/core/src/exchanges/cache.ts index 6b12300c3a..d96b6dc521 100755 --- a/packages/core/src/exchanges/cache.ts +++ b/packages/core/src/exchanges/cache.ts @@ -155,7 +155,7 @@ export const cacheExchange: Exchange = ({ forward, client, dispatchDebug }) => { }; // Reexecutes a given operation with the default requestPolicy -const reexecuteOperation = (client: Client, operation: Operation) => { +export const reexecuteOperation = (client: Client, operation: Operation) => { return client.reexecuteOperation( makeOperation(operation.kind, operation, { ...operation.context, diff --git a/packages/core/src/exchanges/ssr.ts b/packages/core/src/exchanges/ssr.ts index b8fc7abe82..54aad5fad9 100644 --- a/packages/core/src/exchanges/ssr.ts +++ b/packages/core/src/exchanges/ssr.ts @@ -2,6 +2,7 @@ import { GraphQLError } from 'graphql'; import { pipe, share, filter, merge, map, tap } from 'wonka'; import { Exchange, OperationResult, Operation } from '../types'; import { CombinedError } from '../utils'; +import { reexecuteOperation } from './cache'; export interface SerializedResult { data?: string | undefined; // JSON string of data @@ -18,6 +19,7 @@ export interface SSRData { export interface SSRExchangeParams { isClient?: boolean; initialState?: SSRData; + staleWhileRevalidate?: boolean; } export interface SSRExchange extends Exchange { @@ -91,6 +93,7 @@ const deserializeResult = ( /** The ssrExchange can be created to capture data during SSR and also to rehydrate it on the client */ export const ssrExchange = (params?: SSRExchangeParams): SSRExchange => { + const staleWhileRevalidate = !!(params && params.staleWhileRevalidate); const data: Record = {}; // On the client-side, we delete results from the cache as they're resolved @@ -137,7 +140,13 @@ export const ssrExchange = (params?: SSRExchangeParams): SSRExchange => { filter(op => isCached(op)), map(op => { const serialized = data[op.key]!; - return deserializeResult(op, serialized); + const result = deserializeResult(op, serialized); + if (staleWhileRevalidate) { + result.stale = true; + reexecuteOperation(client, op); + } + + return result; }) ); diff --git a/packages/next-urql/src/types.ts b/packages/next-urql/src/types.ts index 7547851605..5a3795cb0a 100644 --- a/packages/next-urql/src/types.ts +++ b/packages/next-urql/src/types.ts @@ -54,4 +54,5 @@ export interface SSRExchange extends Exchange { export interface WithUrqlClientOptions { ssr?: boolean; neverSuspend?: boolean; + staleWhileRevalidate?: boolean; } diff --git a/packages/next-urql/src/with-urql-client.ts b/packages/next-urql/src/with-urql-client.ts index 9adead8ce1..6ce64446d7 100644 --- a/packages/next-urql/src/with-urql-client.ts +++ b/packages/next-urql/src/with-urql-client.ts @@ -52,7 +52,11 @@ export function withUrqlClient( if (!ssr || typeof window === 'undefined') { // We want to force the cache to hydrate, we do this by setting the isClient flag to true - ssr = ssrExchange({ initialState: urqlServerState, isClient: true }); + ssr = ssrExchange({ + initialState: urqlServerState, + isClient: true, + staleWhileRevalidate: options!.staleWhileRevalidate, + }); } else if (!version) { ssr.restoreData(urqlServerState); }