Docs
Integrating Puck
Server Components

React Server Components

Puck provides partial support for React Server Components (opens in a new tab) (RSC) for the following APIs:

Overview

The <Render> component

The <Render> component is a server component and can be rendered in an RSC environment. However, in order to do so the Puck config must be RSC-friendly.

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

The <Puck> component

The core <Puck> component is a client component (marked up with the "use client"; directive), which is necessary to the high-degree of interactivity. This cannot be rendered in an RSC environment.

Because the components are rendered dynamically, all components defined in the config provided to <Puck> should be safe for rendering on the client.

Implementation

Since there are two ways to render Puck components, we need to consider how to satisfy both environments with a single set of components.

There are three options:

  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 is an RSC-friendly way to implement <DropZone> functionality:

const config = {
  components: {
    Columns: {
      render: ({ puck: { renderDropZone } }) => (
        <div>{renderDropZone({ zone: "my-content" })}</div>
      ),
    },
  },
};

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:

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:

components/HeadingBlock.tsx
"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:

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:

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:

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:

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:

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} />;
}