Skip to content
Danny's Blog
Twitter

Multiple entry points for React Native Storybook

storybook, react native, how to, guide3 min read

Intro

React Native storybook is setup in such a way that the decision is left to the user how it should be loaded. This gives you flexibility on how you do things, however I often get asked how to easily switch between running application code and Storybook.

An alternative approach

For a while now I've been thinking about an easy way to do this by using metro directly.

This will be a quick guide on how to setup a secondary entry point for React Native

At the end you'll be able to run yarn storybook-metro when you want Storybook and yarn start when you want to run your app.

The key to this solution looks like this:

1react-native start --config ./.ondevice/metro.storybook.js

This solution works with React Native cli I haven't worked out a solution for expo yet, but maybe someone can help me with that.

The setup

I'll be working from the react native storybook template based on the react native typescript template. However your own setup should work too if you adjust it in the same way.

To use it you can run

1npx react-native init MyApp --template react-native-template-storybook

At the time of writing when you use this template your directory structure will look something like this

1.ondevice/
2 Storybook.tsx
3 main.js
4 ...
5components/
6index
7App.tsx
8...

The files in .ondevice are related to React Native storybook and currently the App.tsx file imports the default export from .ondevice/Storybook.tsx and renders that as the Application.

What if you want to write an application here and run storybook separately, what do you do?

You might just comment out the storybook import when you aren't using it, or you might add some kind of conditional to load storybook when an environment variable is set.

There is another way, you can actually use metro to do all the work for you.

Creating a second entry point

First I'm going to rename .ondevice/Storybook.tsx to .ondevice/index.js and replace the content with this:

1import { AppRegistry } from "react-native";
2import { getStorybookUI } from "@storybook/react-native";
3import { name as appName } from "../app.json";
4import "./storybook.requires";
5
6const StorybookUIRoot = getStorybookUI({});
7
8AppRegistry.registerComponent(appName, () => StorybookUIRoot);

This will act as the secondary entry point for Storybook and when used registers the Storybook UI as the Application to render just like before.

Now in App.tsx lets just remove all storybook related code and make it look like this

1import React from "react";
2import { SafeAreaView, Text } from "react-native";
3export default () => {
4 return (
5 <SafeAreaView>
6 <Text>Not Storybook!</Text>
7 </SafeAreaView>
8 );
9};

This App file gets loaded by index in the root of the project which is the original entry point. This component will be registered as the app when that entry point is used.

We have two entry-points .ondevice/index.js and index.js. Now we just need a way to swap between them.

Create a file in the .ondevice/ folder called metro.storybook.js with the following content:

1const path = require("path");
2module.exports = {
3 transformer: {
4 getTransformOptions: async () => ({
5 transform: {
6 experimentalImportSupport: false,
7 inlineRequires: false,
8 },
9 }),
10 },
11 resolver: {
12 resolverMainFields: ["sbmodern", "react-native", "browser", "main"],
13 },
14 watchFolders: [path.resolve(__dirname, "..")],
15};

The important thing here is the watchFolders config so that metro knows to look for files in the root of the project.

We're also using the way metro works to our advantage, by default it will look for an index file relative to the projectRoot. Since we didn't set the root it will assume that the current directory is the project root.

We're now mostly done, all thats left is to tell metro which config file to load.

The final pieces

Now we just setup some scripts in package json to make our life easier

The template we used gives us these commands

1"scripts": {
2 ...
3 "start": "react-native start",
4 "prestart": "yarn update-stories",
5 ...
6},

The prestart script uses the a prefix to tell yarn to always run this script before the start script.

Now that we've updated our setup we'll be using start specifically for our application code so we'll adjust this.

Let's add a new script for starting metro from our new storybook config file.

1"storybook-metro": "react-native start --config ./.ondevice/metro.storybook.js",

Here we use the --config option to tell metro to use our storybook specific config.

Now rename the prestart script to look like this:

1"prestorybook-metro": "yarn update-stories",

Now the update-stories script only runs when we need it (when running storybook).

Running storybook

Whenever you want to run storybook you run yarn storybook-metro in one terminal followed by running yarn ios or yarn android in another terminal.

To go back to your app you can just stop storybook metro and run yarn start instead. This will start metro with config from metro.config.js and all you have to do is reload the app to get the application javascript bundle.

Note that since only the bundle changes you won't need to rebuild the entire app.

Closing thoughts

As you can see it's possible to separate your storybook code from your application code with just the built in behaviour of metro and React Native.

A great benefit of this is that you can easily leave storybook code outside of your application code since the entry points are completely separate.

This setup could be the default for storybook however a solution is still needed for expo.

In this guide I'm using the React Native Storybook v6 beta which is currently under development however you can follow a similar approach for v5.3 also.

Contact me

If you want to get in touch you can contact me on twitter or the storybook discord