Using Storybook and MSW in React Native
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.
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:
import { initialize, mswDecorator } from 'msw-storybook-addon' initialize()export const decorators = [mswDecorator]
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:
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.
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 callingserver.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:
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:
$ 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.
// storybook/index.jsimport { addDecorator } from '@storybook/react-native'import { queryClient } from '../lib/react-query' addDecorator((storyFn) => { queryClient.clear() return storyFn()})