Storybook for React Server Components

storybookjs

Storybook

Posted on December 13, 2023

Storybook for React Server Components

React Server Components (RSC) are a new programming model for React-based web UIs. In contrast to traditional React “client” components, they only render on the server. This leads to a variety of performance and security benefits, but it is also a huge departure from the React tools and libraries we use today.

One of the most impacted areas is component-driven development and testing. Tools like Storybook, Testing Library, and Playwright/Cypress Component Testing all assume that the user’s components are being rendered in the browser (or JSDom). But with server components, that’s no longer the case.

This creates the question: what does it mean to do isolated component development and testing for the server?

Today, I’m excited to release RSC support in Storybook’s Next.js framework as an experimental answer to this question. It is a purely client-side implementation, making it compatible with the entire ecosystem of Storybook addons and integrations.

Read on to learn how it works, how to use it, and how you can try it today!

Servers are from Mars, clients are from Venus

RSCs have two major differences from traditional client components, both of which are present in the following example:

// ApiCard.tsx

import { ComponentProps } from 'react';
import { Card } from './Card';
import { findById } from './db';

export async function DbCard({ id }: {id: number}) {
  let props;
  try {
    const contact = await findById(id);
    props = { state: 'success', contact };
  } catch (e) {
    props = { state: 'error' };
  }
  return <Card {...props} />;
}
Enter fullscreen mode Exit fullscreen mode

The first difference is that our component is async, which is not supported on the client.

The second difference is that our component can access Node code directly, in this case the findById function that wraps an authenticated database connection.

RSC does a lot under the hood to implement these two differences. This code only ever runs on the server, and it generates a static JSON-like structure which is streamed down to the client.

Storybook is a pure client application. It produces a static build of pure HTML/CSS/JS with no Node in sight! So, supporting RSC would require figuring out either how to get RSCs to render on the client OR rearchitecting Storybook for servers.

We started by focusing on the client approach. We want to minimize impact to our users, who have written millions of stories and hundreds of addons, all based on the current architecture.

So, how the heck does it work?

Getting async with it

The first challenge to getting RSCs to render on the client is configuring how to support async components. It turns out that this is already supported (unofficially) in Next.js’s canary React version. Special thanks to JamesManningR and julRuss, who contributed this simple solution!

import { Suspense } from 'react';

export const ClientContact = ({ id }) => (
  <Suspense><DbCard id={id} /></Suspense>
);
Enter fullscreen mode Exit fullscreen mode

Starting in Storybook 8, @storybook/nextjs can wrap your stories in Suspense using the experimentalNextRSC feature flag in .storybook/main.js:

// .storybook/main.js
export default {
  features: {
    experimentalNextRSC: true,
  }
};
Enter fullscreen mode Exit fullscreen mode

You can also do this manually in 7.x versions of @storybook/nextjs by wrapping your RSC stories in a decorator.

Note: This solution doesn’t yet work in other Storybook React frameworks (e.g. react-vite, react-webpack5) because they do not use Next.js’s canary version of React. Hopefully, this limitation is removed by the next version of React.

Mocked and loaded

Solving the async problem only gets us halfway there. Our DbCard component also references node code which fetches the data to populate the component. This is a problem in the browser, which cannot execute Node code!

To work around this problem, we recommend establishing a clean data access layer. This is also recommended as a best practice by the architect of RSC.

Once you have the data access later, you can mock it out so that it can run in the browser and so that you can precisely control the data that it returns to exercise different UI states (loading, error, success, etc.).

You can mock the data access later using module mocks or network mocks, both of which are supported in Storybook.

Modules: There is a community addon, storybook-addon-module-mock, that provides jest.mock-style mocking (for Webpack projects only). You can also use webpack/vite aliases for a simpler but more limited solution. We plan to provide ergonomic module mocking in a future version of Storybook.

Network APIs: To mock network requests, we recommend Mock Service Worker (msw). Storybook also supports numerous other network and GraphQL mocking addons.

Bringing this back to our example, here’s what a story might look like using storybook-addon-module-mock:


// DbCard.stories.js

import { StoryObj, Meta } from '@storybook/react';
import { createMock } from 'storybook-addon-module-mock';

import { DbCard } from './DbCard';
import * as db from './db';

export default { component: DbCard };

export const Success {
  args: { id: 1 },
  parameters: {
    moduleMock: {
      mock: () => {
        const mock = createMock(db, 'findById');
        mock.mockReturnValue(Promise.resolve({
          name: 'Beyonce',
          img: 'https://blackhistorywall.files.wordpress.com/2010/02/picture-device-independent-bitmap-119.jpg',
          tel: '+123 456 789',
          email: 'b@beyonce.com'
        }))
        return [mock];
      },
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

Full demo: API + module mocking

For the entire example above including both the module mocked database version and the MSW2-mocked API version, please check our full RSC demo Storybook or its GitHub repo.

Contact card component in Storybook

What’s the catch?

In this post, we’ve successfully written a story for our first RSC in Storybook and shown how this is all implemented under the hood.

This was all pretty straightforward, but the approach has limitations:

Fidelity: The pure client implementation differs dramatically from the server-side, streaming RSC implementation that is running in your application.

Convenience: The mocking solutions here can definitely be improved. Not only is our current module mocking solution verbose, but it doesn’t play nicely with Storybook args/controls.

We plan to work on both of these limitations in subsequent iterations, which is why we’ve labeled this solution as experimental.

Use it today with the Storybook 8.0 alpha

To use Storybook for RSC, upgrade your Storybook to 8.0-alpha today:

npx storybook@next upgrade --prerelease
Enter fullscreen mode Exit fullscreen mode

Then, enable the experimental feature in your

.storybook/main.ts:// .storybook/main.js
export default {
  features: {
    experimentalNextRSC: true,
  },
};
Enter fullscreen mode Exit fullscreen mode

For more information, see the @storybook/nextjs README.

This is the first of our posts detailing the contents of Storybook 8.0, our next major version, and we’ll have lots more to come in the months ahead. Keep up with all the news on the next release by following us on social media or signing up for the Storybook newsletter!

💖 💪 🙅 🚩
storybookjs
Storybook

Posted on December 13, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related