Using Storybook and MSW in React Native - Phelipe Teles

Using Storybook and MSW in React Native

4 min.

The integration between Storybook and Mock Service Worker enables you to develop components isolated from your app and your back-end server.

This is not new on the web, the msw-storybook-addon makes it easy to get started. It’s another story for React Native though, since MSW has only recently started supporting it.

I find this combination of tools invaluable, so I couldn’t resist attempting to make it work in React Native, even though it’s pretty new.

In this post, I’m gonna explain in detail how I did it.

:::note tl;dr: You can read the example repository or this pull request instead. :::

How msw-storybook-addon works?

Our end goal is to port msw-storybook-addon to React Native. So let’s first understand how it works.

This is a library by the MSW team that provides you with a global Storybook decorator. From the docs, here’s how you add to your Storybook configuration:

./storybook/preview.js
import { initialize, mswDecorator } from 'msw-storybook-addon' initialize()export const decorators = [mswDecorator]

And here’s how you use it:

JavaScript
import { rest } from 'msw'import { UserProfile } from './UserProfile' export const SuccessBehavior = () => <UserProfile /> SuccessBehavior.parameters = {  msw: {    handlers: [      rest.get('/user', (req, res, ctx) => {        return res(          ctx.json({            firstName: 'Neil',            lastName: 'Maverick',          })        )      }),    ],  },}

This code is using Storybook v6, but only Storybook v5 is available for React Native. Fortunately, both versions support decorators, they differ mostly about how you configure/use it:

storybook/index.js
import { getStorybookUI, configure } from '@storybook/react-native'import { addDecorator } from '@storybook/react-native'import { withMsw, initialize } from './mswDecorator' import './rn-addons' initialize()addDecorator(withMsw) // import storiesconfigure(() => {  require('../components/Task.stories.js')}, module) const StorybookUIRoot = getStorybookUI({  asyncStorage: null,}) export default StorybookUIRoot

Porting msw-storybook-addon to React Native

A decorator is simply a function that does something before rendering the story, which is a React component.

Here is what we need to do:

  • Initialize the MSW server.
  • Clean it up, which means to reset old request handlers.
  • Set up the new request handlers, if any.

Our implementation should not differ very much from the msw-storybook-addon implementation.

First problem: how to initialize the server?

We can’t use setupWorker because we’re not in a browser, we don’t have service workers. setupServer also won’t work, because we’re not in a Node.js environment.

It turns out that we need to use the setupServer function from the msw/native module. This is still undocumented, you’ll only read about it in this GitHub issue and in this example with more details on how to use it.

Implementation

What follows is an implementation that worked for me. You can ignore all non highlighted code, since it’s only meant to stay compatible with the msw-storybook-addon API.

./mswDecorator.js
import 'react-native-url-polyfill/auto'import { setupServer } from 'msw/native' const server = setupServer() export const initialize = () => {  // Do not warn or error out if a non-mocked request happens.  // If we don't use this, Storybook will be spammy about requests made to  // fetch the JS bundle etc.  server.listen({ onUnhandledRequest: 'bypass' })} export const withMsw = (storyFn, { parameters: { msw } }) => {  server.resetHandlers()   if (msw) {    if (Array.isArray(msw) && msw.length > 0) {      // Support an Array of request handlers (backwards compatibility).      server.use(...msw)    } else if ('handlers' in msw && msw.handlers) {      // Support an Array named request handlers handlers      // or an Object of named request handlers with named arrays of handlers      const handlers = Object.values(msw.handlers)        .filter(Boolean)        .reduce((handlers, handlersList) => handlers.concat(handlersList), [])       if (handlers.length > 0) {        server.use(...handlers)      }    }  }   return storyFn()}

You’ll notice that we import a polyfill. This is required, as explained in this Pull Request with an example:

The polyfill react-native-url-polyfill is required or else calling server.start() will result in an Error: not implemented message followed by Error: Invariant Violation: Module AppRegistry is not a registered callable module (calling runApplication)… due to the barebones React Native URL polyfill that throws Not Implemented exceptions for functions that MSW calls such as search().

Usage

Now things should work exactly like msw-storybook-addon, except that you’ll be using Storybook v5, so it’s a little bit different:

React
import React from 'react'import { storiesOf } from '@storybook/react-native'import { rest } from 'msw'import { UserProfile } from './UserProfile' storiesOf('Routes', module).add('SuccessBehavior', () => <UserProfile />, {  msw: {    handlers: [      rest.get('/user', (req, res, ctx) => {        return res(          ctx.json({            firstName: 'Neil',            lastName: 'Maverick',          })        )      }),    ],  },})

Example repository using the official react-native CLI

To prove my point, I implemented the whole thing in a brand new React Native project using react-native CLI:

Bash
$ npx react-native init projectName

You can check the final result in this GitHub repository. Here’s a video:

{{< video src=“./demo-msw-storybook-react-native.webm” caption=“A demo showing Storybook and MSW working in React Native” >}}

To my surprise, I struggled the most to get Storybook working. I came up with issues related with a Promise polyfill that caused Promises to never resolve and to Promise.finally being undefined. I fixed it by using patch-package to remove the line importing the polyfill, as the @storybook/react-native maintainer recommended. This is unfortunate… I hope that a stable Storybook v6 comes soon enough for React Native.

Besides that, everything worked as expected and I hope it works for your project too! I didn’t have this problem with a project using Expo’s Bare Workflow, not sure why though.

Advice for react-query users

If you use react-query, I think it’s also wise to call QueryClient.clear in a decorator, to avoid surprises with the cache.

javascript
// storybook/index.jsimport { addDecorator } from '@storybook/react-native'import { queryClient } from '../lib/react-query' addDecorator((storyFn) => {  queryClient.clear()   return storyFn()})