Skip to content

Commit

Permalink
docs: rewrite RSC docs
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvxd committed Oct 9, 2024
1 parent 238573f commit 1dfcc51
Showing 1 changed file with 168 additions and 4 deletions.
172 changes: 168 additions & 4 deletions apps/docs/pages/docs/integrating-puck/server-components.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
# React Server Components

Puck provides out-of-the-box support for [React Server Components](https://react.dev/reference/react/use-server#use-server) (RSC).
Puck provides support for [React Server Components](https://react.dev/reference/react/use-server#use-server) (RSC), but the interactive-nature of Puck requires special consideration.

Because authoring with Puck dynamically renders components client-side, RSC is limited to the [`<Render>`](/docs/api-reference/components/render) component.
## The server environment

## DropZones and RSC
Puck supports the server environment for the following APIs:

If you're using DropZones with React server components, you must use the [`puck.renderDropZone` prop](/docs/api-reference/configuration/component-config#propspuckrenderdropzone) provided to your render function instead of the `<DropZone>` component.
- The [`<Render>`](/docs/api-reference/components/render) component, for rendering pages produced by Puck
- The [`resolveAllData`](/docs/api-reference/functions/resolve-all-data) lib, for running all [data resolvers](/docs/integrating-puck/dynamic-props)

These APIs can be used in an RSC environment, but in order to do so the Puck config that they reference must be RSC-friendly.

This can be done by either avoiding client-only code (React `useState`, Puck `<DropZone>`, etc), or split out client components with the `"use client";` directive.

## The client environment

All other Puck APIs, including the core `<Puck>` component, cannot run in an RSC environment due to their high-degree of interactivity.

As these APIs render on the client, the Puck config provided must be safe for client-use, avoiding any server-specific logic.

## Implementation

Since the Puck config can be referenced on the client or the server, we need to consider how to satisfy both environments.

There are three approaches to this:

1. Avoid using any client-specific functionality (like React `useState` or Puck's `<DropZone>`) in your components
2. Mark your components up with the `"use client";` directive if you need client-specific functionality
3. Create separate configs for client and server rendering

### Avoid client-specific code

Avoiding client-specific code is the easiest way to support RSC across both environments, but may not be realistic for all users. This means:

1. Avoiding React hooks like `useState`, `useContext` etc
2. Replacing Puck's `<DropZone>` with the `renderDropZone` prop

#### Replacing DropZone with renderDropZone

The [`puck.renderDropZone` prop](/docs/api-reference/configuration/component-config#propspuckrenderdropzone) is an RSC-friendly way to implement `<DropZone>` functionality:

```tsx copy
const config = {
Expand All @@ -19,3 +51,135 @@ const config = {
},
};
```

### Marking up components with `"use client";`

Many modern component libraries will require some degree of client-side behaviour. For these cases, you'll need to mark them up with the `"use client";` directive.

To achieve this, you must import each of those component from a separate file:

```tsx copy showLineNumbers filename="puck.config.tsx"
import type { Config } from "@measured/puck";
import type { HeadingBlockProps } from "./components/HeadingBlock";
import HeadingBlock from "./components/HeadingBlock";

type Props = {
HeadingBlock: HeadingBlockProps;
};

export const config: Config<Props> = {
components: {
HeadingBlock: {
fields: {
title: { type: "text" },
},
defaultProps: {
title: "Heading",
},
// You must call the component, rather than passing it in directly. This will change in the future.
render: ({ title }) => <HeadingBlock title={title} />,
},
},
};
```

And add the `"use client";` directive to the top of each component file:

```tsx copy showLineNumbers filename="components/HeadingBlock.tsx" {1}
"use client";

import { useState } from "react";

export type HeadingBlockProps = {
title: string;
};

export default ({ title }: { title: string }) => {
useState(); // useState fails on the server

return (
<div style={{ padding: 64 }}>
<h1>{title}</h1>
</div>
);
};
```

This config can now be rendered inside an RSC component, such as a Next.js app router page:

```tsx copy showLineNumbers filename="app/page.tsx"
import { config } from "../puck.config.tsx";

export default async function Page() {
const data = await getData(); // Some server function

const resolvedData = await resolveAllData(data, config); // Optional call to resolveAllData, if this needs to run server-side

return <Render data={resolvedData} config={config} />;
}
```

### Creating separate configs

Alternatively, consider entirely separate configs for the `<Puck>` and `<Render>` components. This approach can enable you to have different rendering behavior for a component for when it renders on the client or the server.

To achieve this, you can create a shared config type:

```tsx copy showLineNumbers filename="puck.config.ts"
import type { Config } from "@measured/puck";
import type { HeadingBlockProps } from "./components/HeadingBlock";

type Props = {
HeadingBlock: HeadingBlockProps;
};

export type UserConfig = Config<Props>;
```

Define a server component config that uses any server-only components, excluding any unnecessary fields:

```tsx copy showLineNumbers filename="puck.config.server.tsx"
import type { UserConfig } from "./puck.config.ts";
import HeadingBlockServer from "./components/HeadingBlockServer"; // Import server component

export const config: UserConfig = {
components: {
HeadingBlock: {
render: HeadingBlockServer,
},
},
};
```

And a separate client component config, for use within the `<Puck>` component on the client:

```tsx copy showLineNumbers filename="puck.config.client.tsx"
import type { UserConfig } from "./puck.config.server.ts";
import HeadingBlockClient from "./components/HeadingBlockClient";

export const config: UserConfig = {
components: {
HeadingBlock: {
fields: {
title: { type: "text" },
},
defaultProps: {
title: "Heading",
},
render: ({ title }) => <HeadingBlockClient title={title} />, // Note you must call the component, rather than passing it in directly
},
},
};
```

Now you can render with different configs depending on the context. Here's a Next.js app router example of a server render:

```tsx copy showLineNumbers filename="app/page.tsx"
import { config } from "../puck.config.server.tsx";

export default async function Page() {
const data = await getData(); // Some server function

return <Render data={resolvedData} config={config} />;
}
```

0 comments on commit 1dfcc51

Please sign in to comment.