How to swap between React Native Storybook and your app
Danny

Danny @dannyhw

About: Frontend developer and maintainer of React Native Storybook.

Location:
Portugal
Joined:
Aug 2, 2018

How to swap between React Native Storybook and your app

Publish Date: Feb 6 '23
30 22

EDIT: I have recently updated this guide to show a few different approaches I will update further if I find better solutions

Storybook in react native is a component that you can slot into your app and isn't its own separate runtime which you might be used to on the web.

If you want to run storybook in the same environment as your app we're going to need a simple way to switch between them.

To access an environment variable with expo we can follow their documentation here. If you aren't using expo I recommend following this guide instead.

Note: This guide assumes you have an expo application setup for storybook (see the github readme for info on how to get setup).

Using Expo constants

First rename app.json to app.config.js and edit the file so it looks something like this:

module.exports = {
  name: "my-app",
  slug: "my-app",
  version: "1.0.0",
  orientation: "portrait",
  ...
};
Enter fullscreen mode Exit fullscreen mode

Now add the extra config option like this

module.exports = {
  name: 'MyApp',
  version: '1.0.0',
  extra: {
    storybookEnabled: process.env.STORYBOOK_ENABLED,
  },
  ...
};
Enter fullscreen mode Exit fullscreen mode

Now we can access the storybookEnabled variable from our app using expo constants like this:

import Constants from 'expo-constants';
const apiUrl = Constants.expoConfig.extra.storybookEnabled;
Enter fullscreen mode Exit fullscreen mode

Earlier I said to comment out the app function in the App.js file and this where we go back and fix that.

Edit App.js so that it looks like this:

import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import Constants from 'expo-constants';

function App() {
  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

let AppEntryPoint = App;

if (Constants.expoConfig.extra.storybookEnabled === 'true') {
  AppEntryPoint = require('./.storybook').default;
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

export default AppEntryPoint;
Enter fullscreen mode Exit fullscreen mode

Now in package.json we can add a script like this

"scripts": {
    "storybook": "STORYBOOK_ENABLED='true' expo start",
}
Enter fullscreen mode Exit fullscreen mode

We've added a new command that will run our app with the STORYBOOK_ENABLED flag set to true.

When you run yarn start you should see the app code and yarn storybook should show you storybook.

Removing storybook from the bundle

If you want to remove storybook from your bundle when its disabled you can also add this code to your metro config.

metroConfig.resolver.resolveRequest = (context, moduleName, platform) => {
  const defaultResolveResult = context.resolveRequest(context, moduleName, platform);

  if (
    process.env.STORYBOOK_ENABLED !== 'true' &&
    defaultResolveResult?.filePath?.includes?.('.storybook/')
  ) {
    return {
      type: 'empty',
    };
  }

  return defaultResolveResult;
};
Enter fullscreen mode Exit fullscreen mode

Using entryPoint (expo go <48 only)

This solution won't work in v48 of expo or with expo dev clients.

First we want to rename our app.json to app.config.js and edit the file so it looks something like this:

module.exports = {
  name: "my-app",
  slug: "my-app",
  version: "1.0.0",
  orientation: "portrait",
  ...
};
Enter fullscreen mode Exit fullscreen mode

Add the entryPoint config option like this

module.exports = {
  name: 'MyApp',
  version: '1.0.0',
  entryPoint: process.env.STORYBOOK_ENABLED
      ? "index.storybook.js"
      : "node_modules/expo/AppEntry.js",
  ...
};
Enter fullscreen mode Exit fullscreen mode

Add a file called index.storybook.js in the root of the project with the following content

import { registerRootComponent } from "expo";

import Storybook from "./.storybook";

// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(Storybook);

Enter fullscreen mode Exit fullscreen mode

Now in package.json we can add a script like this

"scripts": {
  "storybook": "STORYBOOK_ENABLED='true' expo start",
}
Enter fullscreen mode Exit fullscreen mode

Which will run our app with the STORYBOOK_ENABLED flag set to true.

Now when you run yarn start you should see the app and yarn storybook should show you storybook.

You can pass the option --ios or --android to have the simulator autmatically open up otherwise press i or a after the command finishes running to open the simulator.

Summary

As you can see there are multiple potential approaches here and I've tried to offer some possible solutions. Each of these have some potential drawbacks but you should assess your projects needs to see what fits for you.

If you are using Expo Go on version 47 or below the entryPoint might be an interesting option for you but it will be removed in 48. The version using expo constants has the possibility increase your bundle size however will work in expo 48.

Heres some comments from Evan Bacon about the entryPoint field:

Other possible approaches include using env variables in metro config to change the project root or using storybook as a separate app outside of your project. You could also manually alternate the main field of the package.json between two different entry point paths (similar to the entryPoint option).

As I find better solutions I will add to this post to showcase the best options.

Thanks for reading.

You can follow me on twitter here: https://twitter.com/Danny_H_W

Comments 22 total

  • АнонимFeb 6, 2023

    [deleted]

  • Divyanshu Mahajan
    Divyanshu MahajanFeb 6, 2023

    Thanks for this article! I wish we could specify the storybook entry file though a field in package.json or storybook config file.

    • Danny
      DannyFeb 7, 2023

      No problem :). Yes I have also been wishing for that, unfortunately I didn’t find a way on expo yet, but I have another guide on how you can do that for rn-cli using two metro configs but its a bit more involved.

      In fact you can use the main field in package.json to specify your entry point when using expo but its very manual.

    • Danny
      DannyFeb 12, 2023

      I updated this post with a new approach that I think matches what you are looking for!

  • Sebastien Lorber
    Sebastien LorberFeb 7, 2023

    Useful trick.

    Little concern: will the Storybook be appropriately tree-shaked from the prod JS bundle when reading from expoConfig? I'm not sure.

    • Danny
      DannyFeb 7, 2023

      Hey Sebastian, my understanding is that it shouldn't be included since it uses an inline require, but I could be wrong.

      I think you could also use a lazy import like AppEntryPoint = React.lazy(() => import('./.storybook')); which would potentially be safer.

      Then you can also put __DEV__ && storybookEnabled to be sure you don't execute that code outside of a dev environment.

      Generally I use storybook as a separate app/package in a mono repo so my projects don't have this problem, so I'll have to double check this and make sure.

    • Danny
      DannyFeb 12, 2023

      @sebastienlorber thanks for your feedback I've updated the guide to use a different entry point instead of expo constants

  • Alexander Zubko
    Alexander ZubkoFeb 11, 2023

    Interesting tip, but you may end up with Storybook code in your production bundle. I think Metro would just crawl all files starting from index and it will look for require calls and pack all your imports into the bundle at compile time, when it doesn’t know if that if condition with env var resolves to true or false. Unless someone coded intentionally the support to ignore requires inside the if(__DEV__).

    I think it is safer to load Storybook from a separate index.storybook.js entry point or smth to not even have a chance of that code to be included to the main bundle. That’s of course is very doable for a regular RN app, but I’ve never used Storybook with Expo Managed though to try if it’s possible there.

    • Danny
      DannyFeb 12, 2023

      Whilst this is possible I'm not sure thats the case until I've fully tested it. I recommend anyone to also test this kind of thing before you copy code into your production project.

      I will be following up this post soon with some testing around the bundle and whether this really makes a difference.

    • Danny
      DannyFeb 12, 2023

      Thanks for your feedback, I've updated the guide with an alternative approach using an index.storybook.js like your suggested.

  • Alexis C
    Alexis CFeb 12, 2023

    Interesting,
    Is there any cons to using metro's sourceExts to differenciate the regular entryPoint index.ts and the storybook one index.storybook.ts ?
    I think it's a nice way to be sure that no storybook code get shipped in the release version, it also allow us to mock other files if needed.

    • Danny
      DannyFeb 14, 2023

      That sounds interesting, do you have an example? I would love to add an example like this to the post.

      • Alexis C
        Alexis CFeb 20, 2023

        Yep, I did a repo you can check if you want, everything should work github.com/ACHP/AwesomeStorybookPr...

        • Danny
          DannyMar 20, 2023

          Hey sorry it took me a while to get to this but this is a really nice solution. The only potential downside is the user should understand that .storybook.tsx would have special meaning throughout the project.

          Nice suggestion! I'll add it :)

  • David Leuliette 🤖
    David Leuliette 🤖Mar 31, 2023

    Awesome article thank you Danny 🚀

  • Philip Lucks
    Philip LucksNov 1, 2023

    Is there a way we can use the expo-dev-menu to toggle the App & Storybook?

  • Eder Henrique Biason de Oliveira
    Eder Henrique Biason de OliveiraJan 23, 2024

    I'm receiving the following error when I try to run 'yarn storybook':

    sb-rn-get-stories && STORYBOOK_ENABLED='true' expo start
    'STORYBOOK_ENABLED' is not recognized as an internal or external command,
    operable program or batch file.

    • Danny
      DannyJan 24, 2024

      if you are using windows STORYBOOK_ENABLED='true' will throw an error, you can use cross-env to make it work like cross-env STORYBOOK_ENABLED='true' expo start

  • H S
    H SJun 10, 2024

    Hi there. Came across this while setting storybook for an expo project. Ended it with manually (script-able) updating the main value in the package.json along with having the flag indicating the storybook-enabled through env-vars.

    The app is expo-router based so main's value becomes expo-router/entry whereas the storybook-setup becomes node_modules/expo/AppEntry.js.

    Nicely compiled guide. Cheers.

  • Ben Keys
    Ben KeysJan 20, 2025

    I needed to use EXPO_PUBLIC_STORYBOOK_ENABLED in order to get this to work.

Add comment