Skip to content

@storyblok/react (Version 8.x RC)

@storyblok/react is Storyblok’s official SDK for React applications.

Version 8.x separates rendering from data fetching. Fetch content with @storyblok/api-client, and render it with the components returned by defineStoryblokBlocks. To upgrade from version 7.x, follow the migration guide.

  • React version 18 or later (version 19 for React Server Components)
  • Node.js LTS (version 22.x recommended)
  • Evergreen web browser (for example, Chrome, Firefox, Safari, or Edge)

Add the SDK and the Storyblok API client to a project by running this command in the terminal:

Terminal window
npm install @storyblok/react@rc @storyblok/api-client

How to configure the SDK, register components, and render stories. The SDK provides two entry points for different rendering modes:

Export When to use How to use
@storyblok/react All environments: client-rendered and single-page apps, static export, and Server Components. Use defineStoryblokBlocks to render content. Use StoryblokPreview or useStoryblokState for live editing in client-rendered apps.
@storyblok/react/rsc Next.js App Router and other React Server Components (RSC) setups that support Server Actions (React 19). Use StoryblokPreview for live editing. Its renderContent prop is a Server Action, so the content re-renders on the server with every change in the Visual Editor.

Create an @storyblok/api-client instance with the space’s access token, and register the React components that render each block.

src/lib/storyblok.js
import { createApiClient } from "@storyblok/api-client";
import { defineStoryblokBlocks } from "@storyblok/react";
import Page from "@/components/Page";
import Teaser from "@/components/Teaser";
export const apiClient = createApiClient({
accessToken: "YOUR_ACCESS_TOKEN",
region: "eu",
});
export const { StoryblokBlock, StoryblokBlocks, StoryblokRichText } = defineStoryblokBlocks({
components: {
page: Page,
teaser: Teaser,
},
});

Create a React component for each block defined in Storyblok and registered in the configuration. Each component receives a block prop that contains the block’s content, and an editable prop.

src/components/Teaser.jsx
export default function Teaser({ block, editable }) {
return (
<div {...editable}>
<h2>{block.headline}</h2>
</div>
);
}

To automatically render nested blocks, use StoryblokBlocks, which renders each block of an array through the registered components.

src/components/Page.jsx
import { StoryblokBlocks } from "@/lib/storyblok";
export default function Page({ block, editable }) {
return <main {...editable}>{block.body && <StoryblokBlocks blocks={block.body} />}</main>;
}

Use the API client to fetch a story and render its content with StoryblokBlock.

In a React Server Component, fetch the story directly:

src/app/page.js
import { notFound } from "next/navigation";
import { apiClient, StoryblokBlock } from "@/lib/storyblok";
export default async function Home() {
const { data } = await apiClient.stories.get("home", {
query: { version: "draft" },
});
if (!data?.story) {
notFound();
}
return <StoryblokBlock block={data.story.content} />;
}

In a client-rendered application, use a data-fetching library such as SWR or React Query alongside the API client. These libraries handle loading and error states, and cache the fetched stories:

src/App.jsx
import useSWR from "swr";
import { apiClient, StoryblokBlock } from "@/lib/storyblok";
async function fetchStory(slug) {
const { data } = await apiClient.stories.get(slug, {
query: { version: "draft" },
});
if (!data) throw new Error(`Story not found: ${slug}`);
return data.story;
}
export default function App() {
const { data: story, error } = useSWR("home", fetchStory);
if (error) return <div>Failed to load story.</div>;
if (!story) return <div>Loading...</div>;
return <StoryblokBlock block={story.content} />;
}

To enable live editing in the Visual Editor, wrap the rendered content in StoryblokPreview.

@storyblok/react exports the following components, hooks, and helpers.

defineStoryblokBlocks() maps Storyblok blocks to React components and returns the StoryblokBlock, StoryblokBlocks, and StoryblokRichText components, which share the same component map.

import { defineStoryblokBlocks } from "@storyblok/react";
export const { StoryblokBlock, StoryblokBlocks, StoryblokRichText } = defineStoryblokBlocks(OPTIONS);

defineStoryblokBlocks() accepts the following options:

Key Description Type
components An object that maps the technical names of Storyblok blocks to React components or to a component configuration. Each component receives a block prop containing the content of the block, and an editable prop. Required. Record<string, StoryblokBlockEntry>
fallback A component to render when a block has no registered component. The fallback receives the same block and editable props. React component
suspenseFallback The default element to show while an asynchronous or lazy component loads, for example, <Skeleton />. React element

If a block has no registered component and the options don’t include a fallback, StoryblokBlock renders nothing and logs a No component registered for "<block-name>" warning.

Instead of a component, an entry in components can be a configuration object that controls the Suspense behavior of that component. This configuration is especially useful for asynchronous Server Components that fetch their own data.

export const { StoryblokBlock, StoryblokBlocks, StoryblokRichText } = defineStoryblokBlocks({
components: {
hero: {
component: Hero,
fallback: <HeroSkeleton />,
suspense: true,
},
article: Article,
},
suspenseFallback: <Skeleton />,
});
Key Description Type
component The React component that renders the block. Required. React component
fallback The element to show while this component loads. Overrides suspenseFallback. React element
suspense Whether to wrap the component in a Suspense boundary. StoryblokBlock wraps components created with React.lazy automatically. boolean

StoryblokBlock is a React component that dynamically renders a single block from Storyblok. defineStoryblokBlocks returns it.

StoryblokBlock accepts a block prop, which should be a block from the Storyblok API. StoryblokBlock passes any other props directly to the block component.

<StoryblokBlock block={block} />

StoryblokBlocks is a React component that renders an array of blocks, such as the content of a blocks field. defineStoryblokBlocks returns it.

StoryblokBlocks accepts a blocks prop and renders each block with StoryblokBlock, using the _uid of the block as the key. StoryblokBlocks passes any other props to every block component.

<StoryblokBlocks blocks={block.nested_blocks} />

StoryblokPreview is a React component that enables live editing in the Visual Editor. The component receives the fetched story and a renderContent function, and calls renderContent again with the updated story on every change in the Visual Editor.

Outside the Visual Editor, StoryblokPreview doesn’t load the Storyblok Bridge and renders the content once.

StoryblokPreview provides two entry points depending on the application architecture: one for client-rendered applications and one for server-rendered applications that use React Server Components.

For client-rendered applications, import StoryblokPreview from @storyblok/react:

import { StoryblokPreview } from "@storyblok/react";
import { StoryblokBlock } from "@/lib/storyblok";
<StoryblokPreview story={story} renderContent={(live) => <StoryblokBlock block={live.content} />} />;

The renderContent function receives the live story and returns the React content to render.

For applications that use React Server Components, import StoryblokPreview from @storyblok/react/rsc. In this entry point, StoryblokPreview is an asynchronous Server Component, and renderContent must be a Server Action. This entry point requires React 19.

Define the Server Action in a separate file:

src/lib/actions.js
"use server";
import { StoryblokBlock } from "@/lib/storyblok";
export async function renderContent(story) {
return <StoryblokBlock block={story.content} />;
}

Then pass the Server Action to StoryblokPreview:

src/app/page.js
import { notFound } from "next/navigation";
import { StoryblokPreview } from "@storyblok/react/rsc";
import { renderContent } from "@/lib/actions";
import { apiClient } from "@/lib/storyblok";
export default async function Home() {
const { data } = await apiClient.stories.get("home", {
query: { version: "draft" },
});
if (!data?.story) {
notFound();
}
return <StoryblokPreview story={data.story} renderContent={renderContent} />;
}

StoryblokPreview renders the initial content on the server. In the Visual Editor, the component calls the Server Action on every change and streams the result in, keeping the current content on screen until the update arrives. If the Server Action fails, StoryblokPreview logs the error and keeps the initially rendered content visible.

Key Description Type
story The story fetched from the Storyblok API. Required. Story
renderContent A function that receives the live story and returns the content to render. In @storyblok/react/rsc, it must be an asynchronous Server Action. Required. function
bridgeOptions Options for the Storyblok Bridge, such as resolveRelations. StoryblokPreview reads them once, when it mounts. Learn more in the @storyblok/preview-bridge reference. object
debounceMs Milliseconds to wait after the last change in the Visual Editor before updating the content. In @storyblok/react/rsc, the default is 200; otherwise, updates are immediate. number

When fetching a story with the resolve_relations parameter, also pass the same relations as bridgeOptions.resolveRelations, so the relations stay resolved after a live update. Enable inlineRelations in the API client to inline the resolved stories into the content.

useStoryblokState() accepts a story from the Storyblok API and makes the story reactive for live editing. The hook returns the latest version of the story on every change in the Visual Editor. StoryblokPreview from @storyblok/react uses this hook internally.

Use StoryblokPreview to wrap the rendered content of a story. Use useStoryblokState() when other parts of the component also depend on the live story, for example, to update the page title while editing.

import { useStoryblokState } from "@storyblok/react";
import { StoryblokBlock } from "@/lib/storyblok";
export default function Home({ story: STORY_OBJECT }) {
const story = useStoryblokState(STORY_OBJECT, OPTIONS);
return <StoryblokBlock block={story.content} />;
}

OPTIONS accepts the same bridgeOptions and debounceMs options as StoryblokPreview.

useStoryblokEditorEvent() subscribes to changes in the Visual Editor and calls a function with the updated story, without keeping the story in state. Use the hook to react to changes in a custom way, for example, to update data held by another state manager.

import { useStoryblokEditorEvent } from "@storyblok/react";
useStoryblokEditorEvent((story) => {
// ...
}, OPTIONS);

OPTIONS accepts the same bridgeOptions and debounceMs options as StoryblokPreview.

Every component rendered through StoryblokBlock receives an editable prop. Spread editable onto the root element of the component to make the component editable in the Visual Editor.

export default function Feature({ block, editable }) {
return <section {...editable}>{block.title}</section>;
}

For components that aren’t rendered through StoryblokBlock, use storyblokEditable() instead. storyblokEditable() accepts a block from the Storyblok API and returns an object containing the same HTML attributes.

import { storyblokEditable } from "@storyblok/react";
export default function Feature({ block }) {
return <section {...storyblokEditable(block)}>{block.title}</section>;
}

StoryblokRichText renders a rich text field from a Storyblok story. Use the StoryblokRichText component returned by defineStoryblokBlocks, so that blocks embedded in the rich text field render through the registered components. The component works in both Server Components and Client Components.

import { StoryblokRichText } from "@/lib/storyblok";
<StoryblokRichText document={block.richtext_field} />;

The component accepts the following optional props:

  • components sets custom React components for supported rich text nodes and marks.
  • optimizeImage sets image optimization options for image nodes.
  • data shares custom data with all custom rich text components. Each component receives the data as context.data.

To render rich text without the component map, use the standalone createRichTextRenderer function. The function renders embedded blocks only when its components option includes a custom component for the blok node, the rich text node that holds embedded blocks.

When using TypeScript, custom components are fully type-safe through the StoryblokReactRichTextProps<T> utility type. For example, StoryblokReactRichTextProps<"link"> provides the attributes and children available for the link mark, while StoryblokReactRichTextProps<"heading"> provides access to heading-specific attributes such as level.

Find the supported rich text nodes and marks, and the customization options, in the @storyblok/richtext reference.

To register custom components, use the components prop. The key must match a supported rich text node or mark name.

import type { StoryblokReactRichTextComponentMap } from "@storyblok/react";
const components: StoryblokReactRichTextComponentMap = {
heading: CustomHeading,
link: CustomLink,
table: CustomTable,
bold: ({ children }) => <b className="font-bold text-black">{children}</b>,
};

Then pass the components to StoryblokRichText:

<StoryblokRichText document={block.richtext_field} components={components} />

Marks receive their rendered child content through the children prop.

import type { StoryblokReactRichTextProps } from "@storyblok/react";
import Link from "next/link";
export function CustomLink({ children, attrs }: StoryblokReactRichTextProps<"link">) {
return (
<Link href={attrs?.href ?? ""} target={attrs?.target ?? "_self"}>
{children}
</Link>
);
}

Nodes can use the children prop to render child content.

import type { StoryblokReactRichTextProps } from "@storyblok/react";
export function CustomHeading({ children, attrs }: StoryblokReactRichTextProps<"heading">) {
const Tag = `h${attrs?.level ?? 1}` as keyof JSX.IntrinsicElements;
return <Tag className="custom-heading">{children}</Tag>;
}

Example: custom table component (advanced node)

Section titled “Example: custom table component (advanced node)”

For advanced use cases, nodes can access content and context to recursively render child nodes using StoryblokRichText.

To render semantic table nodes, use the splitTableRows utility from @storyblok/richtext and create <thead> and <tbody> elements.

import type { StoryblokReactRichTextProps } from "@storyblok/react";
import { StoryblokRichText } from "@/lib/storyblok";
import { splitTableRows } from "@storyblok/richtext";
export function CustomTable({ content, context }: StoryblokReactRichTextProps<"table">) {
const { headerRows, bodyRows } = splitTableRows(content);
return (
<table className="custom-table">
{headerRows.length > 0 && (
<thead>
<StoryblokRichText document={headerRows} {...context} />
</thead>
)}
<tbody>
<StoryblokRichText document={bodyRows} {...context} />
</tbody>
</table>
);
}

Spreading context passes the custom components, image options, and data of the parent on to the nested rows, including the renderer for embedded blocks.

createRichTextRenderer() returns a function that renders a rich text document to React elements. Use createRichTextRenderer() to render rich text outside of JSX.

import { createRichTextRenderer } from "@storyblok/react";
const render = createRichTextRenderer({ components, optimizeImage: true });
const content = render(block.richtext_field);

createRichTextRenderer() accepts the same components, optimizeImage, and data options as StoryblokRichText. To render embedded blocks, provide a custom component for the blok rich text node.

@storyblok/react exports types for stories and blocks. Use StoryblokBlockComponentProps to type the props of a block component:

import type { StoryblokBlockData, StoryblokBlockComponentProps } from "@storyblok/react";
type PageProps = StoryblokBlockComponentProps<{ body: StoryblokBlockData[] }>;
export default function Page({ block, editable }: PageProps) {
// ...
}

To pass extra props through StoryblokBlock and StoryblokBlocks in TypeScript, declare the extra props as a type argument of defineStoryblokBlocks, for example, defineStoryblokBlocks<{ locale: string }>(OPTIONS). Then pass the same props as the second type argument of StoryblokBlockComponentProps, for example, StoryblokBlockComponentProps<{ title: string }, { locale: string }>.

To narrow story.content to the blocks of a project, use the withTypes() method of the API client. Learn more in the @storyblok/api-client reference.

Last updated:

Was this page helpful?

What went wrong?

This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window).Terms of Service (opens in a new window) apply.