DEV Community

Enes Zorlu
Enes Zorlu

Posted on

📚 How to Handle Multiple MSW Handlers in Storybook Stories

Handling multiple states in Storybook stories can often be problematic, especially when different handlers are required to simulate various states. This tutorial will guide you through the process of managing these handlers effectively to ensure each story showcases the correct state without persistent issues.

1. Problem Overview

When using multiple handlers to showcase different states in a Storybook story, the first handler persists, leading to incorrect states being displayed. This often requires manually reloading the page to reset the handlers, which is not an ideal solution. We will solve this issue by implementing a custom decorator to force reload the story.

2. What is Storybook and MSW?

Storybook is an open-source tool for developing UI components in isolation for frameworks like React, Vue, and Angular. It streamlines UI development, testing, and documentation by allowing developers to create and visualize components in a dedicated environment, independent of the main application. This enables more efficient debugging, testing, and showcasing of individual components, leading to a more robust and maintainable codebase. Aka a fancy component library tailored to your project or organisation.

MSW (Mock Service Worker) is a powerful tool for mocking API requests in both client-side and server-side applications. It intercepts network requests at the network layer, allowing developers to simulate different responses and states such as loading, error, and success. By integrating MSW with Storybook, developers can create realistic scenarios for their components, ensuring comprehensive testing and consistent behavior across different states.

3. Creating a basic React Component

Let's start by creating a simple CardList component that fetches data and handles loading, error, and empty data states.

import React, { useEffect, useState } from 'react';

const CardList = () => {
  const [cards, setCards] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/cards')
      .then((response) => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      })
      .then((data) => {
        setCards(data.cards);
        setLoading(false);
      })
      .catch((error) => {
        setError(error);
        setLoading(false);
      });
  }, []);

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  if (cards.length === 0) {
    return <div>No cards available</div>;
  }

  return (
    <div className="card-list">
      {cards.map((card) => (
        <div key={card.id} className="card">
          <h2>{card.title}</h2>
          <p>{card.description}</p>
        </div>
      ))}
    </div>
  );
};

export default CardList;

Enter fullscreen mode Exit fullscreen mode

4. Creating a Basic Story

Next, we will create a basic Storybook configuration for our CardList component.

import { StoryObj } from '@storybook/react';
import CardList from './CardList';

const meta = {
  title: 'Components/CardList',
  component: CardList,
};

export default meta;

type Story = StoryObj<typeof CardList>;

export const Default: Story = {};
Enter fullscreen mode Exit fullscreen mode

5. Implementing MSW Mock

To simulate data fetching, we will implement msw mocks for our Default story. This will enable us to setup a mock server which will respond to fetch requests in our component, therefore we will see mock data in our story.

**Note: **MSW Addon needs to be setup correctly for this to work, please refer to Storybook MSW Addon

import { rest } from 'msw';

const handlers = {
  success: [
    rest.get('/api/cards', (req, res, ctx) => {
      return res(
        ctx.json({
          cards: [
            { id: '1', title: 'Card 1', description: 'Description 1' },
            { id: '2', title: 'Card 2', description: 'Description 2' },
          ],
        })
      );
    }),
  ],
};

export const Default: Story = {
  parameters: {
    msw: handlers.success,
  },
};

Enter fullscreen mode Exit fullscreen mode

6. Adding Other Handlers and Stories

Of course only adding default state is not enough, we want to see all of the possible states in Storybook. You might be wondering, how? Exactly same as we did before, more handlers more fun!

We will now add additional handlers and corresponding stories to simulate various states.

const handlers = {
  success: [
    rest.get('/api/cards', (req, res, ctx) => {
      return res(
        ctx.json({
          cards: [
            { id: '1', title: 'Card 1', description: 'Description 1' },
            { id: '2', title: 'Card 2', description: 'Description 2' },
          ],
        })
      );
    }),
  ],
  empty: [
    rest.get('/api/cards', (req, res, ctx) => {
      return res(ctx.json({ cards: [] }));
    }),
  ],
  loading: [
    rest.get('/api/cards', (req, res, ctx) => {
      return res(ctx.delay('infinite'));
    }),
  ],
  serverError: [
    rest.get('/api/cards', (req, res, ctx) => {
      return res(ctx.status(500));
    }),
  ],
};

export const Empty: Story = {
  parameters: {
    msw: handlers.empty,
  },
};

export const Loading: Story = {
  parameters: {
    msw: handlers.loading,
  },
};

export const ServerError: Story = {
  parameters: {
    msw: handlers.serverError,
  },
};
Enter fullscreen mode Exit fullscreen mode

7. Problem Explanation

But wait, does this work? Partially... When using multiple handlers to showcase different states in a Storybook story, the first handler persists, leading to incorrect states being displayed. This often requires manually reloading the page to reset the handlers, which is not an ideal solution!

8. Fixing the Problem with forceReloadDecorator

Of course, I did not write this article to talk about problems. We are here to solve problems baby! To solve the problem of handlers persisting across different stories, we will create a custom decorator that forces a reload. Feels bit rogue, bit hacky.. but you know what, it works better than any other over-engineered solution out there.

Now when switching between the stories, story container will be reloaded very quickly. This will trigger re-initialisation of handlers and solve our problem.

const forceReloadDecorator = (storyFn, context) => {
  if (context.globals.shouldReload) {
    context.globals.shouldReload = false;
    window.location.reload();
  }
  context.globals.shouldReload = true;
  return storyFn();
};

const meta = {
  title: 'Components/CardList',
  component: CardList,
  decorators: [forceReloadDecorator],
};

export default meta;
Enter fullscreen mode Exit fullscreen mode

To ensure the implementation works, run your Storybook and navigate through the different stories. Each story should now reflect the correct state without persisting the handlers from previous stories.

In this tutorial, we addressed the issue of persistent handlers in Storybook stories and implemented a solution using a force reload decorator. This approach ensures that each story displays the correct state, improving the development and testing experience.

Further Reading

Storybook Documentation
MSW Worker

Top comments (0)