With the release of Storybook 8, Storybook is now (experimentally) compatible with React Server Components (RSCs), amongst other new updates. The implementation for this feature is purely on the client side, which means it’s compatible with Storybook’s entire ecosystem of add-ons and integrations.
In this article, we will discuss Storybook 8’s compatibility with RSCs. We’ll also explore the possibilities it introduces for component-driven development and testing in this changing landscape.
This article expects that you understand the concept of stories in Storybook. If you don’t, here’s a quick look at how stories work to catch you up.
As a pure client application, Storybook traditionally dealt with static builds in HTML, CSS, and JavaScript, as it assumes components are being rendered in the browser. But since RSCs are exclusively rendered on the server, the dynamic is completely changed.
So, why is this update important? First, we need to understand the dynamics between Storybook’s client-centric nature and server-rendered components.
This contrast raises questions about developing components in isolation and testing components in server environments. What implications does it hold for developers accustomed to working with browser-rendered components? Let’s take a look.
RSCs represent a significant evolution in React’s rendering capabilities. You can take a look at this comprehensive guide to RSCs or see my quick summary of the relevant points below.
Unlike traditional React components that render completely on the client side, RSCs allow certain components to be rendered on the server. This means when a user requests a page, the initial HTML can be rendered on the server and then sent to the client, improving time-to-first-byte (TTFB) and perceived performance.
By pre-rendering components on the server, RSCs can significantly reduce a page’s time to interactivity because the server sends back an already-generated HTML document.
In other words, when the client receives the HTML document, it can render the page faster because it doesn’t have to wait for the JavaScript to execute to generate the content. The only potential setback is network latency, or when the server response takes a long time to reach the client due to network issues.
With RSCs, users experience faster load times and smoother interactions, leading to a more engaging user experience. React Server Components offer a powerful solution for improving performance, SEO, accessibility, and overall UX.
When building a client-side component like a Button
with Storybook, you are creating a sandbox environment where you can visualize and interact with it in various states and configurations. Here’s how it typically works:
Button
component from the rest of your application so it’s not dependent on any of the app’s other components or services. You can view and test the Button
component in isolation, making it easier to focus on its design and behavior without interference from other parts of your applicationButton
component. For example, you can have stories for a primary button, a secondary button, a disabled button, a button with an icon, etc. Each story represents a different configuration or state of the Button
component, allowing you to test its behavior under various conditionsButton
component and see how it responds to user interactions such as clicks, hovers, and keyboard events. This allows you to thoroughly test the functionality of the Button
component and ensure that it behaves as expected in different scenariosRSCs, on the other hand, often rely on dynamic data or context provided by the server during rendering. Unlike client components, they may require server-side resources to render correctly. Here’s what I mean.
Say we’re building a blog homepage with various features. For example, we need to render a list of blogs. This means the homepage can have different states — like loading, error, empty, and default states — depending on the response from the API.
Storybook, being primarily designed for static component rendering, may struggle to handle the dynamic nature of RSCs. This limitation makes it challenging to accurately represent RSC behavior within the Storybook environment.
But with Storybook 8, you can now build, test, and document Next.js server applications in isolation. Note that I specified Next.js here — Storybook 8 brings React Server Component compatibility to Next.js only, so it’s considered experimental at this point.
Let’s build a demo store page for an ecommerce website where we simply list different products the store offers. We’re going to simulate these different states in Storybook:
Keep in mind that this page is rendered on the server, so we will use Storybook’s experimental support for RSCs along with the Mock Service Worker (MSW) Storybook add-on to mock REST or GraphQL requests within our story.
We’ll start by setting up Storybook in a Next.js project and configuring it to work with RSCs. Then, we’ll create stories for the store page in different states. Here’s the project repo so that you can follow along nicely!
First, install Storybook in your Next.js project:
npx storybook@next init
Configure Storybook’s main.ts
file to point to the new stories and enable the experimental RSC feature:
// main.ts const config: StorybookConfig = { stories: ['../app/**/*.stories.tsx'], features: { experimentalRSC: true } }
We’ll have two components in our project: a single Product
component and a Products
component that lists all the products in our store.
Here’s the code for the Product
component:
// product.tsx import { FC } from 'react'; export interface ProductProps { id: string; title: string; price: string; description: string; } export const Product: FC<ProductProps> = ({ id, title, price, description, }) => { return ( <div className='border min-w-[200px] max-w-[200px] p-4 rounded-lg flex flex-col gap-2'> <p>{title}</p> <p>{price}</p> <p>{description}</p> </div> ); };
And here’s how an example Product
component looks:
Here’s the code for the Products
component:
import { FC } from "react" import { Product, ProductProps } from "./product" export const Products:FC<{products: Array<ProductProps>}> = ({ products }) => { return ( <div className="flex gap-2 flex-wrap"> { products.length > 0 ? ( products.map((item, index) => ( <Product key={item.title + index} {...item} /> )) ): ( <p>Oops! Looks like we're all out of stock</p> ) } </div> ) }
And here’s how the Products
component looks:
Next, we’ll bring the Products
component into our HomePage
, which is a server component that handles getting data from our API and passing it to the Products
component:
import { Products } from "@/components/products"; import fetchData from "@/lib/fetch-data"; export default async function Home() { const products = await fetchData() return ( <main className="flex flex-col gap-6 min-h-screen p-8"> <h1 className="text-3xl font-bold">Products Listing</h1> <Products products={products} /> </main> ); }
The resulting products listing should look like this when all products are rendered:
Here’s what the fetchData
function looks like:
import { cache } from 'react' const fetchData = cache(async () => { const res = await fetch( "https://fakestoreapi.com/products", { next: { revalidate: 10 } } ) if (res.status !== 200) { throw new Error(`Status ${res.status}`) } const products = await res.json() return products }); export default fetchData;
Note that we’re using a free products API from FakeStore API.
Next, we’ll create stories for the homepage component simulating our different states — default, error, loading, empty. Let’s start by simulating the default state of the homepage.
Since we’re fetching data from a network API, we’ll mock its requests with MSW. Create a story with mocked data for the homepage:
// app/homepage/page.stories.tsx import type { Meta, StoryObj } from '@storybook/react'; import Home from './page'; const Wrapper = ({ productCount }: { productCount: number }) => ( <Home /> ) const meta: Meta = { title: 'app/HomePage', component: Wrapper, }; export default meta; type Story = StoryObj<typeof meta>; export const APIMocked = { parameters: { msw: { handlers: [ http.get('https://api.example.com/products', () => { return HttpResponse.json([{ id: "1", price: "10", title: "Maison Margiela Boots", description: "Black suede boots" }]); }), ], }, }, };
This is just an example to show how you would handle fetching data from an API and writing a story that represents the state of the page at that time — in this case, while getting a list of products.
If we wanted more products, we could simply add to the list, but hard-coding API responses like this is tedious. So instead, we can handle generating the data ourselves by building a simplified in-memory database using MSW’s data factory library.
From this library, we can read data and generate the desired network response. Then, we can write stories to populate the database with test cases.
Let’s see how to use MSW in Storybook to mock requests for fetching products. First, install the MSW Storybook add-on and initialize it:
npm i msw [email protected] -D npx msw init public/
Note that although MSW has compatibility issues with the server-side aspects of Next.js’s new app directory, it works perfectly fine when we use it in the browser.
This is because Storybook operates entirely on the client side, so it can use MSW to mock network requests without any issues. We can test UI components and network interactions effectively in Storybook without worrying about compatibility issues.
Next, we need to initialize MSW with the onUnhandledRequest
option in the .storybook/preview.tsx
:
// .storybook/preview.tsx import { initialize, mswLoader } from 'msw-storybook-addon'; initialize({ onUnhandledRequest: 'warn' }); const preview = { loaders: [mswLoader], }
Next, let’s build an in-memory database using MSW’s data factory library. Create MSW handlers that read from the database and generate network responses:
// data.mock.ts import { faker } from '@faker-js/faker'; import { drop, factory, primaryKey } from '@mswjs/data'; let _id = 1; const db = factory({ product: { id: primaryKey(() => _id++), title: faker.commerce.productName, price: () => faker.commerce.price({ min: 100, max: 200, dec: 0, symbol: '$' }), description: faker.commerce.productDescription } }); export const reset = () => drop(db); export const generateProducts = (count: number): number[] => { return Array.from({ length: count }, (_, i) => i); }; export const createProduct = (product = {}) => db.product.create(product); export const getProducts = () => { const products = db.product.getAll(); return products; };
Then, update the .storybook/preview.tsx
file to use MSW handlers for reading from the database:
// .storybook/preview.tsx import type { Preview } from "@storybook/react"; import { initialize, mswLoader } from 'msw-storybook-addon'; import "../src/app/globals.css" import { http, HttpResponse } from 'msw' import { getProducts } from "../src/lib/mock" initialize({ onUnhandledRequest: 'warn' }); const preview: Preview = { parameters: { msw: { handlers: [ http.get('https://fakestoreapi.com/products', () => { const products = getProducts(); return HttpResponse.json(products); }) ] }, }, loaders: [mswLoader] }; export default preview;
With this setup, Storybook will use MSW to mock API responses, utilizing the in-memory database to provide realistic test data for your components.
Now that we’re done with our setup, we can go ahead and mock as much data as we like, using the functions we built and test out different states of our application:
// app/homepage/page.stories.tsx import type { Meta, StoryObj } from '@storybook/react'; import { createProduct, generateProducts, reset } from '../../../lib/data.mock'; import Home from './page'; const meta: Meta = { title: 'app/HomePage', component: HomePage, }; export default meta; type Story = StoryObj<typeof meta>; export const Default: Story = { args: { productCount: 30, }, loaders: [({ args: { productCount } }) => { reset(); generateProducts(productCount).map(() => createPost()) }], }; export const Error = { render: () => <ErrorPage /> }; export const Empty: Story = { loaders: [ () => { reset() } ], };
Now, depending on the state of the page, what you see on the Products Listing page will change. Here’s how the Empty
state we mocked above should look:
Storybook 8‘s experimental support for RSCs in Next.js means developers can now create stories for RSCs while benefiting from Storybook’s rich ecosystem of add-ons. RSCs offer improved performance and user experience by rendering components on the server.
While integrating Storybook with RSCs presents challenges, the new release allows for building, testing, and documenting server components in isolation as we have demonstrated. Have fun playing around with this feature — cheers!
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.