Storybook
Posted on December 13, 2023
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} />;
}
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>
);
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,
}
};
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];
},
},
},
}
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.
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
Then, enable the experimental feature in your
.storybook/main.ts:// .storybook/main.js
export default {
features: {
experimentalNextRSC: true,
},
};
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!
Posted on December 13, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.