From 35c64d3a00ecc4f778b695d01588e9017669bb1b Mon Sep 17 00:00:00 2001 From: Chris Villa Date: Wed, 9 Oct 2024 12:52:35 +0100 Subject: [PATCH] docs: rewrite RSC docs --- .../integrating-puck/server-components.mdx | 172 +++++++++++++++++- 1 file changed, 168 insertions(+), 4 deletions(-) diff --git a/apps/docs/pages/docs/integrating-puck/server-components.mdx b/apps/docs/pages/docs/integrating-puck/server-components.mdx index 5f7ba86cf..13586fa60 100644 --- a/apps/docs/pages/docs/integrating-puck/server-components.mdx +++ b/apps/docs/pages/docs/integrating-puck/server-components.mdx @@ -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 [``](/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 `` component. +- The [``](/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 ``, etc), or split out client components with the `"use client";` directive. + +## The client environment + +All other Puck APIs, including the core `` component, cannot run in an RSC environment due to their high-degree of interactivity. + +Since these APIs render on the client, the Puck config they use must be safe for client use, and avoid any server-specific logic, including all Puck components. + +## 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 ``) 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 `` 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 `` functionality: ```tsx copy const config = { @@ -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 = { + 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 }) => , + }, + }, +}; +``` + +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 ( +
+

{title}

+
+ ); +}; +``` + +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 ; +} +``` + +### Creating separate configs + +Alternatively, consider entirely separate configs for the `` and `` 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; +``` + +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 `` 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 }) => , // 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 ; +} +```