Skip to content

Commit

Permalink
major(next): support Next 13 and React Server Components (#3214)
Browse files Browse the repository at this point in the history
Co-authored-by: Phil Pluckthun <[email protected]>
  • Loading branch information
JoviDeCroock and kitten authored Jul 21, 2023
1 parent aeb6b44 commit 889ca4d
Show file tree
Hide file tree
Showing 31 changed files with 1,023 additions and 1,617 deletions.
9 changes: 9 additions & 0 deletions .changeset/generated-one-myself.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@urql/next': major
---

Create `@urql/next` which is a package meant to support Next 13 and
the React 18 features contained within.

For server components we have `@urql/next/rsc` and for client components
just `@urql/next`.
160 changes: 153 additions & 7 deletions docs/advanced/server-side-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,16 +177,162 @@ 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
`next-urql`. The `next-urql` package includes setup for `react-ssr-prepass` already, which automates
a lot of the complexity of setting up server-side rendering with `urql`.
`@urql/next`. The `@urql/next` package is set to work with Next 13.

We have a custom integration with [`Next.js`](https://nextjs.org/), being [`next-urql`](https://github.com/urql-graphql/urql/tree/main/packages/next-urql)
this integration contains convenience methods specifically for `Next.js`.
These will simplify the above setup for SSR.

To set up `next-urql`, first we'll install `next-urql` with `react-is` and `urql` as
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 Head from 'next/head';
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 { UrqlProvider, ssrExchange, cacheExchange, fetchExchange, createClient } from '@urql/next';

const ssr = ssrExchange();
const client = createClient({
url: 'https://trygql.formidable.dev/graphql/web-collections',
exchanges: [cacheExchange, ssr, fetchExchange],
});

export default function Layout({ children }: React.PropsWithChildren) {
return (
<UrqlProvider client={client} ssr={ssr}>
{children}
</UrqlProvider>
);
}
```

It is important that we pas 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/router';

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
Expand Down
47 changes: 0 additions & 47 deletions examples/with-next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,50 +28,3 @@ yarn run start
npm install
npm run start
```

## getInitialProps

This is the output you'll get when you're using `{ ssr: true }`, this way urql will try to automate
as much for you as it possibly can by using a [`prepass`](https://github.com/FormidableLabs/react-ssr-prepass)
this means that every `useQuery` used in your virtual-dom will be ran, the data will be collected on the server
and hydrated on the client.

> NOTE: to reduce performance complexities try to keep this to top-level renders as this can amount to waterfalls.
## getStaticProps

This requires some manual work, when we look at [`static.js`](./pages/static.js) we can see that we define our own
`getStaticProps` method, this because these methods are only `user-facing`. When doing a `yarn next build` we'll need to
ensure that the server we're targetting is running so we can successfully execute the static prerender.

## getServerSideProps

This requires some manual work, when we look at [`server.js`](./pages/server.js) we can see that we define our own
`getServerSideProps` method, this because these methods are only `user-facing`.

## Output

We can see that our `/` and `/server` routes are rendered on the server and `/static` is statically prerendered.

```
Page Size First Load JS
┌ λ / 4.98 kB 90 kB
├ /_app 0 B 85 kB
├ ○ /404 3.46 kB 88.5 kB
├ λ /api/graphql 0 B 85 kB
├ λ /server 878 B 85.9 kB
└ ● /static 895 B 85.9 kB
+ First Load JS shared by all 85 kB
├ chunks/d8c192fcf6e34535672c13f111ef41e3832b265d.d03071.js 17.4 kB
├ chunks/f6078781a05fe1bcb0902d23dbbb2662c8d200b3.6a2b27.js 13.3 kB
├ chunks/framework.4b1bec.js 41.8 kB
├ chunks/main.3d1d43.js 7.14 kB
├ chunks/pages/_app.92bde8.js 4.68 kB
└ chunks/webpack.50bee0.js 751 B
λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
○ (Static) automatically rendered as static HTML (uses no initial props)
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
(ISR) incremental static regeneration (uses revalidate in getStaticProps)
```
16 changes: 16 additions & 0 deletions examples/with-next/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
24 changes: 24 additions & 0 deletions examples/with-next/app/non-rsc/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';

import {
UrqlProvider,
ssrExchange,
cacheExchange,
fetchExchange,
createClient,
} from '@urql/next';

const ssr = ssrExchange();
const client = createClient({
url: 'https://graphql-pokeapi.graphcdn.app/',
exchanges: [cacheExchange, ssr, fetchExchange],
suspense: true,
});

export default function Layout({ children }: React.PropsWithChildren) {
return (
<UrqlProvider client={client} ssr={ssr}>
{children}
</UrqlProvider>
);
}
65 changes: 65 additions & 0 deletions examples/with-next/app/non-rsc/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'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) {
results {
id
name
}
}
}
`;

function Pokemons() {
const [result] = useQuery({ query: PokemonsQuery });
return (
<main>
<h1>This is rendered as part of SSR</h1>
<ul>
{result.data
? result.data.pokemons.results.map((x: any) => (
<li key={x.id}>{x.name}</li>
))
: JSON.stringify(result.error)}
</ul>
<Suspense>
<Pokemon name="bulbasaur" />
</Suspense>
<Link href="/">RSC</Link>
</main>
);
}

const PokemonQuery = gql`
query ($name: String!) {
pokemon(name: $name) {
id
name
}
}
`;

function Pokemon(props: any) {
const [result] = useQuery({
query: PokemonQuery,
variables: { name: props.name },
});
return (
<div>
<h1>{result.data && result.data.pokemon.name}</h1>
</div>
);
}
40 changes: 40 additions & 0 deletions examples/with-next/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Link from 'next/link';
import { cacheExchange, createClient, fetchExchange, gql } from '@urql/core';
import { registerUrql } from '@urql/next/rsc';

const makeClient = () => {
return createClient({
url: 'https://graphql-pokeapi.graphcdn.app/',
exchanges: [cacheExchange, fetchExchange],
});
};

const { getClient } = registerUrql(makeClient);

const PokemonsQuery = gql`
query {
pokemons(limit: 10) {
results {
id
name
}
}
}
`;

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
? result.data.pokemons.results.map((x: any) => (
<li key={x.id}>{x.name}</li>
))
: JSON.stringify(result.error)}
</ul>
<Link href="/non-rsc">Non RSC</Link>
</main>
);
}
5 changes: 5 additions & 0 deletions examples/with-next/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
Loading

0 comments on commit 889ca4d

Please sign in to comment.