📚 How to Handle Multiple MSW Handlers in Storybook Stories
Tags: storybook, webdev, frontend, programming
Published: 2024-06-13 22:27:37

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.

```js
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;

```

## 4. Creating a Basic Story
Next, we will create a basic Storybook configuration for our CardList component.

```js
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 = {};
```

## 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](https://storybook.js.org/addons/msw-storybook-addon)

```js
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,
  },
};

```

## 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.

```js
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,
  },
};
```

## 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.

```js
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;
```

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](https://storybook.js.org/docs/get-started)
[MSW Worker](https://storybook.js.org/addons/msw-storybook-addon)