-
Notifications
You must be signed in to change notification settings - Fork 462
/
Copy pathserver-side-rendering.md
636 lines (502 loc) · 20.4 KB
/
server-side-rendering.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
---
title: Server-side Rendering
order: 3
---
# Server-side Rendering
In server-side rendered applications we often need to set our application up so that data will be
fetched on the server-side and later sent down to the client for hydration. `urql` supports this
through the `ssrExchange.`
## The SSR Exchange
The `ssrExchange` has two functions. On the server-side it's able to gather all results as they're
being fetched, which can then be serialized and sent to the client. On the client-side it's able to
use these serialized results to rehydrate and render the application without refetching this data.
To start out with the `ssrExchange` we have to add the exchange to our `Client`:
```js
import { Client, cacheExchange, fetchExchange, ssrExchange } from '@urql/core';
const isServerSide = typeof window === 'undefined';
// The `ssrExchange` must be initialized with `isClient` and `initialState`
const ssr = ssrExchange({
isClient: !isServerSide,
initialState: !isServerSide ? window.__URQL_DATA__ : undefined,
});
const client = new Client({
exchanges: [
cacheExchange,
ssr, // Add `ssr` in front of the `fetchExchange`
fetchExchange,
],
});
```
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()`:
```js
// Extract and serialise the data like so from the `ssr` instance
// we've previously created by calling `ssrExchange()`
const data = JSON.stringify(ssr.extractData());
const markup = ''; // The render code for our framework goes here
const html = `
<html>
<body>
<div id="root">${markup}</div>
<script>
window.__URQL_DATA__ = JSON.parse(${data});
</script>
</body>
</html>
`;
```
This will provide `__URQL_DATA__` globally, which we've used in our first example to inject data into
the `ssrExchange` on the client-side.
Alternatively you can also call `restoreData` as long as this call happens synchronously before the
`client` starts receiving queries.
```js
const isServerSide = typeof window === 'undefined';
const ssr = ssrExchange({ isClient: !isServerSide });
if (!isServerSide) {
ssr.restoreData(window.__URQL_DATA__);
}
```
## Using `react-ssr-prepass`
In the previous examples we've set up the `ssrExchange`, however with React this still requires us
to manually execute our queries before rendering a server-side React app [using `renderToString`
or `renderToNodeStream`](https://reactjs.org/docs/react-dom-server.html#rendertostring).
For React, `urql` has a "Suspense mode" that [allows data fetching to interrupt
rendering](https://reactjs.org/docs/concurrent-mode-suspense.html). However, Suspense is
not supported by React during server-side rendering.
Using [the `react-ssr-prepass` package](https://github.com/FormidableLabs/react-ssr-prepass) however,
we can implement a prerendering step before we let React server-side render, which allows us to
automatically fetch all data that the app requires with Suspense. This technique is commonly
referred to as a "two-pass approach", since our React element is traversed twice.
To set this up, first we'll install `react-ssr-prepass`. It has a peer dependency on `react-is`
and `react`.
```sh
yarn add react-ssr-prepass react-is react-dom
# or
npm install --save react-ssr-prepass react-is react-dom
```
Next, we'll modify our server-side code and add `react-ssr-prepass` in front of `renderToString`.
```jsx
import { renderToString } from 'react-dom/server';
import prepass from 'react-ssr-prepass';
import {
Client,
cacheExchange,
fetchExchange,
ssrExchange,
Provider,
} from 'urql';
const handleRequest = async (req, res) => {
// ...
const ssr = ssrExchange({ isClient: false });
const client new Client({
url: 'https://??',
suspense: true, // This activates urql's Suspense mode on the server-side
exchanges: [cacheExchange, ssr, fetchExchange]
});
const element = (
<Provider value={client}>
<App />
</Provider>
);
// Using `react-ssr-prepass` this prefetches all data
await prepass(element);
// This is the usual React SSR rendering code
const markup = renderToString(element);
// Extract the data after prepass and rendering
const data = JSON.stringify(ssr.extractData());
res.status(200).send(`
<html>
<body>
<div id="root">${markup}</div>
<script>
window.__URQL_DATA__ = JSON.parse(${data});
</script>
</body>
</html>
`);
};
```
It's important to set enable the `suspense` option on the `Client`, which switches it to support
React suspense.
### With Preact
If you're using Preact instead of React, there's a drop-in replacement package for
`react-ssr-prepass`, which is called `preact-ssr-prepass`. It only has a peer dependency on Preact,
and we can install it like so:
```sh
yarn add preact-ssr-prepass preact
# or
npm install --save preact-ssr-prepass preact
```
All above examples for `react-ssr-prepass` will still be the same, except that instead of
using the `urql` package we'll have to import from `@urql/preact`, and instead of `react-ssr-prepass`
we'll have to import from. `preact-ssr-prepass`.
## Next.js
If you're using [Next.js](https://nextjs.org/) you can save yourself a lot of work by using
`@urql/next`. The `@urql/next` package is set to work with Next 13.
To set up `@urql/next`, first we'll install `@urql/next` and `urql` as
peer dependencies:
```sh
yarn add @urql/next urql graphql
# or
npm install --save @urql/next urql graphql
```
We now have two ways to leverage `@urql/next`, one being part of a Server component
or being part of the general `app/` folder.
In a server component we will import from `@urql/next/rsc`
```ts
// app/page.tsx
import React from 'react';
import { cacheExchange, createClient, fetchExchange, gql } from '@urql/core';
import { registerUrql } from '@urql/next/rsc';
const makeClient = () => {
return createClient({
url: 'https://trygql.formidable.dev/graphql/basic-pokedex',
exchanges: [cacheExchange, fetchExchange],
});
};
const { getClient } = registerUrql(makeClient);
export default async function Home() {
const result = await getClient().query(PokemonsQuery, {});
return (
<main>
<h1>This is rendered as part of an RSC</h1>
<ul>
{result.data.pokemons.map((x: any) => (
<li key={x.id}>{x.name}</li>
))}
</ul>
</main>
);
}
```
When we aren't leveraging server components we will import the things we will
need to do a bit more setup, we go to the `client` component's layout file and
structure it as the following.
```tsx
// app/client/layout.tsx
'use client';
import { useMemo } from 'react';
import { UrqlProvider, ssrExchange, cacheExchange, fetchExchange, createClient } from '@urql/next';
export default function Layout({ children }: React.PropsWithChildren) {
const [client, ssr] = useMemo(() => {
const ssr = ssrExchange({
isClient: typeof window !== 'undefined',
});
const client = createClient({
url: 'https://trygql.formidable.dev/graphql/web-collections',
exchanges: [cacheExchange, ssr, fetchExchange],
suspense: true,
});
return [client, ssr];
}, []);
return (
<UrqlProvider client={client} ssr={ssr}>
{children}
</UrqlProvider>
);
}
```
It is important that we pass both a client as well as the `ssrExchange` to the `Provider`
this way we will be able to restore the data that Next streams to the client later on
when we are hydrating.
The next step is to query data in your client components by means of the `useQuery`
method defined in `@urql/next`.
```tsx
// app/client/page.tsx
'use client';
import Link from 'next/link';
import { Suspense } from 'react';
import { useQuery, gql } from '@urql/next';
export default function Page() {
return (
<Suspense>
<Pokemons />
</Suspense>
);
}
const PokemonsQuery = gql`
query {
pokemons(limit: 10) {
id
name
}
}
`;
function Pokemons() {
const [result] = useQuery({ query: PokemonsQuery });
return (
<main>
<h1>This is rendered as part of SSR</h1>
<ul>
{result.data.pokemons.map((x: any) => (
<li key={x.id}>{x.name}</li>
))}
</ul>
</main>
);
}
```
The data queried in the above component will be rendered on the server
and re-hydrated back on the client. When using multiple Suspense boundaries
these will also get flushed as they complete and re-hydrated.
> When data is used throughout the application we advise against
> rendering this as part of a server-component so you can benefit
> from the client-side cache.
### Invalidating data from a server-component
When data is rendered by a server component but you dispatch a mutation
from a client component the server won't automatically know that the
server-component on the client needs refreshing. You can forcefully
tell the server to do so by using the Next router and calling `.refresh()`.
```tsx
import { useRouter } from 'next/navigation';
const Todo = () => {
const router = useRouter();
const executeMutation = async () => {
await updateTodo();
router.refresh();
};
};
```
### Disabling RSC fetch caching
You can pass `fetchOptions: { cache: "no-store" }` to the `createClient`
constructor to avoid running into cached fetches with server-components.
## Legacy Next.js (pages)
If you're using [Next.js](https://nextjs.org/) with the classic `pages` you can instead use `next-urql`.
To set up `next-urql`, first we'll install `next-urql` with `react-is` and `urql` as peer dependencies:
```sh
yarn add next-urql react-is urql graphql
# or
npm install --save next-urql react-is urql graphql
```
The peer dependency on `react-is` is inherited from `react-ssr-prepass` requiring it.
Note that if you are using Next before v9.4 you'll need to polyfill fetch, this can be
done through [`isomorphic-unfetch`](https://www.npmjs.com/package/isomorphic-unfetch).
We're now able to wrap any page or `_app.js` using the `withUrqlClient` higher-order component. If
we wrap `_app.js` we won't have to wrap any individual page.
```js
// pages/index.js
import React from 'react';
import { useQuery } from 'urql';
import { withUrqlClient } from 'next-urql';
const Index = () => {
const [result] = useQuery({
query: '{ test }',
});
// ...
};
export default withUrqlClient((_ssrExchange, ctx) => ({
// ...add your Client options here
url: 'http://localhost:3000/graphql',
}))(Index);
```
The `withUrqlClient` higher-order component function accepts the usual `Client` options as
an argument. This may either just be an object, or a function that receives the Next.js'
`getInitialProps` context.
One added caveat is that these options may not include the `exchanges` option because `next-urql`
injects the `ssrExchange` automatically at the right location. If you're setting up custom exchanges
you'll need to instead provide them in the `exchanges` property of the returned client object.
```js
import { cacheExchange, fetchExchange } from '@urql/core';
import { withUrqlClient } from 'next-urql';
export default withUrqlClient(ssrExchange => ({
url: 'http://localhost:3000/graphql',
exchanges: [cacheExchange, ssrExchange, fetchExchange],
}))(Index);
```
Unless the component that is being wrapped already has a `getInitialProps` method, `next-urql` won't add its own SSR logic, which automatically fetches queries during
server-side rendering. This can be explicitly enabled by passing the `{ ssr: true }` option as a second argument to `withUrqlClient`.
When you are using `getStaticProps`, `getServerSideProps`, or `getStaticPaths`, you should opt-out of `Suspense` by setting the `neverSuspend` option to `true` in your `withUrqlClient` configuration.
During the prepass of your component tree `next-urql` can't know how these functions will alter the props passed to your page component. This injection
could change the `variables` used in your `useQuery`. This will lead to error being thrown during the subsequent `toString` pass, which isn't supported in React 16.
### SSR with { ssr: true }
The `withUrqlClient` only wraps our component tree with the context provider by default.
To enable SSR, the easiest way is specifying the `{ ssr: true }` option as a second
argument to `withUrqlClient`:
```js
import { cacheExchange, fetchExchange } from '@urql/core';
import { withUrqlClient } from 'next-urql';
export default withUrqlClient(
ssrExchange => ({
url: 'http://localhost:3000/graphql',
exchanges: [cacheExchange, ssrExchange, fetchExchange],
}),
{ ssr: true } // Enables server-side rendering using `getInitialProps`
)(Index);
```
Be aware that wrapping the `_app` component using `withUrqlClient` with the `{ ssr: true }`
option disables Next's ["Automatic Static
Optimization"](https://nextjs.org/docs/advanced-features/automatic-static-optimization) for
**all our pages**. It is thus preferred to enable server-side rendering on a per-page basis.
### SSR with getStaticProps or getServerSideProps
Enabling server-side rendering using `getStaticProps` and `getServerSideProps` is a little
more involved, but has two major benefits:
1. allows **direct schema execution** for performance optimisation
2. allows performing extra operations in those functions
To make the functions work with the `withUrqlClient` wrapper, return the `urqlState` prop
with the extracted data from the `ssrExchange`:
```js
import { withUrqlClient, initUrqlClient } from 'next-urql';
import { ssrExchange, cacheExchange, fetchExchange, useQuery } from 'urql';
const TODOS_QUERY = `
query { todos { id text } }
`;
function Todos() {
const [res] = useQuery({ query: TODOS_QUERY });
return (
<div>
{res.data.todos.map(todo => (
<div key={todo.id}>
{todo.id} - {todo.text}
</div>
))}
</div>
);
}
export async function getStaticProps(ctx) {
const ssrCache = ssrExchange({ isClient: false });
const client = initUrqlClient(
{
url: 'your-url',
exchanges: [cacheExchange, ssrCache, fetchExchange],
},
false
);
// This query is used to populate the cache for the query
// used on this page.
await client.query(TODOS_QUERY).toPromise();
return {
props: {
// urqlState is a keyword here so withUrqlClient can pick it up.
urqlState: ssrCache.extractData(),
},
revalidate: 600,
};
}
export default withUrqlClient(
ssr => ({
url: 'your-url',
})
// Cannot specify { ssr: true } here so we don't wrap our component in getInitialProps
)(Todos);
```
The above example will make sure the page is rendered as a static-page, It's important that
you fully pre-populate your cache 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.
The `getServerSideProps` and `getStaticProps` functions only run on the **server-side** — any
code used in them is automatically stripped away from the client-side bundle using the
[next-code-elimination tool](https://next-code-elimination.vercel.app/). This allows **executing
our schema directly** using `@urql/exchange-execute` if we have access to our GraphQL server:
```js
import { withUrqlClient, initUrqlClient } from 'next-urql';
import { ssrExchange, cacheExchange, fetchExchange, useQuery } from 'urql';
import { executeExchange } from '@urql/exchange-execute';
import { schema } from '@/server/graphql'; // our GraphQL server's executable schema
const TODOS_QUERY = `
query { todos { id text } }
`;
function Todos() {
const [res] = useQuery({ query: TODOS_QUERY });
return (
<div>
{res.data.todos.map(todo => (
<div key={todo.id}>
{todo.id} - {todo.text}
</div>
))}
</div>
);
}
export async function getServerSideProps(ctx) {
const ssrCache = ssrExchange({ isClient: false });
const client = initUrqlClient(
{
url: '', // not needed without `fetchExchange`
exchanges: [
cacheExchange,
ssrCache,
executeExchange({ schema }), // replaces `fetchExchange`
],
},
false
);
await client.query(TODOS_QUERY).toPromise();
return {
props: {
urqlState: ssrCache.extractData(),
},
};
}
export default withUrqlClient(ssr => ({
url: 'your-url',
}))(Todos);
```
Direct schema execution skips one network round trip by accessing your resolvers directly
instead of performing a `fetch` API call.
### 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
is an uncommon scenario, and we consider it "unsafe" so evaluate this carefully for yourself.
When this does seem like the appropriate solution any component wrapped with `withUrqlClient` will receive the `resetUrqlClient`
property, when invoked this will create a new top-level client and reset all prior operations.
## Vue Suspense
In Vue 3 a [new feature was introduced](https://vuedose.tips/go-async-in-vue-3-with-suspense/) that
natively allows components to suspend while data is loading, which works universally on the server
and on the client, where a replacement loading template is rendered on a parent while data is
loading.
We've previously seen how we can change our usage of `useQuery`'s `PromiseLike` result to [make use
of Vue Suspense on the "Queries" page.](../basics/vue.md#vue-suspense)
Any component's `setup()` function can be updated to instead be an `async setup()` function, in
other words, to return a `Promise` instead of directly returning its data. This means that we can
update any `setup()` function to make use of Suspense.
On the server-side we can then use `@vue/server-renderer`'s `renderToString`, which will return a
`Promise` that resolves when all suspense-related loading is completed.
```jsx
import { createSSRApp } = from 'vue'
import { renderToString } from '@vue/server-renderer';
import urql, {
createClient,
cacheExchange,
fetchExchange,
ssrExchange
} from '@urql/vue';
const handleRequest = async (req, res) => {
// This is where we'll put our root component
const app = createSSRApp(Root)
// NOTE: All we care about here is that the SSR Exchange is included
const ssr = ssrExchange({ isClient: false });
app.use(urql, {
exchanges: [cacheExchange, ssr, fetchExchange]
});
const markup = await renderToString(app);
const data = JSON.stringify(ssr.extractData());
res.status(200).send(`
<html>
<body>
<div id="root">${markup}</div>
<script>
window.__URQL_DATA__ = JSON.parse(${data});
</script>
</body>
</html>
`);
};
```
This effectively renders our Vue app on the server-side and provides the client-side data for
rehydration that we've set up in the above [SSR Exchange section](#the-ssr-exchange) to use.