Docs
Back to blog
Join the community
⚛️ React

Build a Next.js app in Storybook with React Server Components and Mock Service Worker

Develop, document, and test RSC applications in isolation, using MSW to mock network requests
My Image
and Michael Shilman
My Image

Storybook 8 (our next major release) brings React Server Component (RSC) compatibility to Storybook for the very first time, letting you build, test and document Next.js server applications in isolation.

In our first demo, we used Storybook to develop a contact card RSC, which accessed contact data both asynchronously and from a file system while simulating server code through module mocks.

When Chuck Norris builds frontend applications, the UI tests itself!

Next, we’ll explore how to build an entire app in isolation with the Next.js App Router, by rebuilding the Hacker Next example in Storybook with the help of Mock Service Worker.

Why build pages in isolation?

It’s amazing how much UI fits into just two pages. Consider the data states your pages need. Then, multiply them by responsive layouts, logged-in views, themes, browsers, locales, and accessibility. It doesn’t take much for a handful of pages to become hundreds of variants.

Storybook solves this complexity by letting you teleport to any UI state by isolating it as a story! If you’re new to Storybook, here’s how stories work.

Interested in taking your Storybook tests even further? Storybook 8 now supports native automated visual tests, so you can catch unexpected visual changes across your entire application in a single button click. Learn more about getting started with the Visual Tests addon.

Writing stories for Hacker Next

First, install Storybook in your Next.js project:

npx storybook@next init
 

Then, add the experimentalRSC flag to Storybook’s main.ts, and point it to the new stories we’re about to write:

// main.ts
 
const config: StorybookConfig = {
- stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+ stories: ['../app/**/*.stories.tsx'],
  // ... existing config
+ features: { experimentalRSC: true }
}

Now, let’s write stories for Hacker Next’s two components: the news homepage and the item page! Here’s what a simple story could look like for the news page:

// app/news/[page]/index.stories.tsx
 
import type { Meta, StoryObj } from '@storybook/react';
import News from './page';
 
const meta = {
  title: 'app/News',
  component: News,
} satisfies Meta<typeof News>;
export default meta;
 
type Story = StoryObj<typeof meta>;
 
export const Home: Story = {
  args: { params: { page: 1 } },
}

Though this works, you’ll notice that it’s missing styling. We can fix that by adding a decorator to our .storybook/preview.tsx:

// .storybook/preview.tsx
 
import Layout from '../app/layout.tsx';
 
export default {
  // other settings
  decorators: [(Story) => <Layout><Story /></Layout>],
}

That’s more like it! Now, try doing this for app/item/[id]/(comments)/page.tsx. If you get stuck, check our repo.

Mock ‘n’ roll with Mock Service Worker

Rather than using real data, we want to be able to control the data. This lets us test different states and generate consistent results.

Hacker Next fetches data from a network API, so we’ll mock its requests with Mock Service Worker (MSW).

💡If you follow the space closely, you might ask, ‘Isn’t MSW currently incompatible with Next.js app directory?’ This is true. However, since we’re running it in the browser rather than in Next.js, there’s no problem using MSW with Storybook.

First, let’s add Storybook’s MSW addon to our project. We’ll use the canary version that supports MSW 2.0’s dramatically improved API.

pnpx storybook add msw-storybook-addon@2.0.0--canary.122.06f0c92.0
pnpx msw init public

Next, update .storybook/preview.tsx to initialize MSW with the onUnhandledRequest option. This ensures our existing story continues to work.

// .storybook/preview.tsx
 
// ... existing imports
+ import { initialize, mswLoader } from 'msw-storybook-addon';
 
+ initialize({ onUnhandledRequest: 'warn' });
 
const preview: Preview = {
+ loaders: [mswLoader],
  decorators: [(Story) => <Layout><Story /></Layout>],
}

Now, let’s create a story for Hacker Next’s homepage, featuring a single post:

// app/news/[page]/index.stories.tsx
import { http, HttpResponse } from 'msw'
 
// ...existing meta/story
 
export const Mocked = {
  ...Home,
  parameters: {
    msw: {
      handlers: [
        http.get('https://hacker-news.firebaseio.com/v0/topstories.json', () => {
          return HttpResponse.json([1]);
        }),
        http.get('https://hacker-news.firebaseio.com/v0/item/1.json', () => {
          return HttpResponse.json({
            id: 1,
            time: Date.now(),
            user: 'shilman',
            url: 'http://storybook.js.org',
            title: 'Storybook + Next.js = ❤️',
            score: 999,
          });
        }),
      ],
    },
  },
};

By mocking two REST API requests from the frontend and hard-coding the responses, we get the following story:

MSW data factories

Hard-coded API responses are difficult to scale. So, let’s write a story that controls page content with a higher level argument! We’ll need to:

Build a simplified in-memory database

Create MSW handlers that read from the database and generate the desired network responses

Write stories to populate the database with test cases

Step 1: Build the database

First, let’s create the database using @mswjs/data (MSW’s data factory library) and Faker.js.

// data.mock.ts
 
import { faker } from '@faker-js/faker'
import { drop, factory, primaryKey } from '@mswjs/data
 
let _id;
const db = factory({
  item: {
    id: primaryKey(() => _id++),
    time: () => faker.date.recent({ days: 2 }).getTime() / 1000,
    user: faker.internet.userName,
    title: faker.lorem.words,
    url: faker.internet.url,
    score: () => faker.number.int(100),
  }
})
 
/** Reset the database */
export const reset = (seed?: number) => {
  _id = 1
  faker.seed(seed ?? 123)
  return drop(db)
}
 
/** Create a post. Faker will fill in any missing data */
export const createPost = (item = {}) => db.item.create(item);
 
/** Utility function */
export const range = (n: number) => Array.from({length: n}, (x, i) => i);
 
/** Return all the post IDs */
export const postIds = () => db.item.findMany({}).map((p) => p.id);
 
/** Return the content of a single post by ID */
export const getItem = (id: number) => db.item.findFirst({ where: { id: { equals: id }}});

This lets you specify the posts exactly as you want them to appear. When we leave any data unspecified, Faker fills in the gaps. This way, you can create tens or hundreds of posts with minimal code!

Step 2: Create MSW handlers

Next, we’ll update .storybook/preview.tsx with MSW handlers that read from the database. These handlers are available across all your stories and read whatever’s in the database. This means a story’s only job is to fill the database with useful data!

// .storybook/preview.tsx
 
import { postIds, getItem } from '../lib/data.mock.ts';
import { http, HttpResponse } from 'msw'
 
const preview: Preview = {
  // ...existing configuration
  parameters: { msw: { handlers: [
    http.get(
      'https://hacker-news.firebaseio.com/v0/topstories.json',
      () => HttpResponse.json(postIds())
    ),
    http.get<{ id: string }>(
      'https://hacker-news.firebaseio.com/v0/item/:id.json',
      ({ params }) => HttpResponse.json(getItem(parseInt(params.id, 10)))
    )
  ] } },
};

Step 3: Write stories

Finally, we’ll write stories for our new setup.

First, replace your existing Mocked story with a new version using a loader (a function that runs before a story renders). This loader calls our createPost helper function, which 1) instantiates a post and 2) adds it to the in-memory database.

// app/news/[page]/index.stories.tsx
 
import { createPost } from '../../../lib/data.mock';
 
// ...existing meta/story
 
export const MockedNew = {
  ...Home,
  loaders: [() => {
    createPost({
      id: -1,
      user: 'shilman',
      url: 'http://storybook.js.org',
      title: 'Storybook + Next.js = ❤️',
      score: 999,
    });
  }],
};

This scheme really shines when you need to create a lot of data at once. To demonstrate this, let’s create a homepage showing 30 posts. And to make it even more powerful, we can allow the number of posts to be controlled interactively in Storybook’s UI:

// app/news/[page]/index.stories.tsx
 
import { createPost, range, reset  } from '../../../lib/data.mock'
 
export const FullPage = {
  args: {
    postCount: 30,
  },
  loaders: [({ args: { postCount } }) => {
    reset();
    range(postCount).forEach(() => createPost());
  }];
}

It’s time to test

Congratulations! You’ve built Hacker Next in Storybook with data that you can customize for different tests. Alternatively, view a demo Storybook (shared via Chromatic) or our repo.

0:00 /0:21 1×

As well as bringing your UI into one place, you’re able to test Hacker Next in ways you couldn’t otherwise.

For example, you can write stories for Hacker Next’s upvote and collapsing comment states by using Storybook’s play function. This is a code snippet that simulates user interactions and runs immediately after a story renders. It can interact with the DOM using Testing-Library and assert using Vitest’s expect and spies.

Here’s a story that uses a play function to upvote the first post on the homepage:

0:00 /0:13 1×

// app/news/[page]/index.stories.tsx
 
import { within, userEvent } from '@storybook/test';
 
export const Upvoted: Story = {
  ...FullPage,
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement)
    const votes = await canvas.findAllByText('▲')
    await userEvent.click(votes[0])
  }
}

Bringing it all together

In this exercise, we’ve cataloged the key UI states of a Next.js application. Once we have this all in Storybook, we can:

Develop against mocked data even if our backend is under development

Develop hard-to-reach UI states such as a ‘credit card expired’ screen

Instantly run visual regression and a11y tests on every screen, testing across browsers and different resolutions

View production stories next to their design files to ensure smooth handoff

Onboard new developers with living and comprehensive documentation of the entire frontend architecture

Learn more about using Next.js with Storybook

Storybook revolutionized the development of reusable components. Now, you can apply those same benefits to the pages of your applications.

In our next RSC post, we’ll explore module mocking to handle real-world cases where it’s impossible or impractical to mock network requests.

Storybook 8 (our next major release) brings React Server Components support to Storybook!

In our new tutorial, learn how to build, document and test an RSC app in isolation with @nextjs, Storybook, and @ApiMockinghttps://t.co/SVZ3TNJw1I

— Storybook (@storybookjs) January 18, 2024

Credits

Thanks to Artem Zakharchenko (MSW’s core maintainer) and the Next.js team for their review and guidance!

Last updated
Share on
Join the Storybook mailing list
Get the latest news, updates and releases

We're hiring!
Join the team behind Storybook and Chromatic. Build tools that are used in production by 100s of thousands of developers. Remote-first.
Keep reading
My ImageFuture of Storybook in 2024
Highlights from 2023 and what’s coming next
My Image
and Michael Shilman
My ImageHow to make Storybook 2-4x faster
Optimize build performance for Storybook 7.6
My Image
and Kasper Peulen
My ImageStorybook for React Server Components
Use RSCs in Storybook by upgrading to Storybook 8.0 alpha
My Image
and Michael Shilman