Setting Up Storybook Web and Native with Expo Router v2, SDK 49, and TypeScript

Setting Up Storybook Web and Native with Expo Router v2, SDK 49, and TypeScript

Featured on Hashnode

Introduction

In my previous post, I shared a comprehensive guide about setting up Storybook to work with Expo SDK 49, Expo Router v2, and TypeScript.

If you haven't read that article yet, I highly recommend checking it out, as I'll be using the repository from the last post as a starting point.

Using Storybook with Expo Router v2, SDK 49 & TypeScript

You can also fork my GitHub repo

Since the Storybook Native setup is already working with Expo SDK 49, Expo Router v2, and TypeScript on my repo, this article will focus on adding Storybook web, so we can easily share our components with the rest of the team, or even publish it with Vercel.

The SpiroKit UI Kit includes an interactive documentation portal that is built with Storybook and is publicly available here. Feel free to explore it for inspiration and gather some ideas.


Adding dependencies

TLDR

  1. Update your package.json to include all the required and recommended dependencies

    {
     "scripts": {
       ...
     },
     "dependencies": {
       ...
     },
     "devDependencies": {
       "@babel/core": "^7.20.0",
       "typescript": "^5.1.3",
       "@types/react": "~18.2.14",
       "@babel/plugin-proposal-export-namespace-from": "^7.18.9",
       "@react-native-async-storage/async-storage": "^1.19.0",
       "@react-native-community/datetimepicker": "^7.4.0",
       "@react-native-community/slider": "^4.4.2",
       "@storybook/addon-actions": "^6.5.16",
       "@storybook/addon-controls": "^6.5.16",
    +   "@storybook/addon-essentials": "^6.5.16",
    +   "@storybook/addon-links": "^6.5.16",
       "@storybook/addon-ondevice-actions": "^6.5.4",
       "@storybook/addon-ondevice-controls": "^6.5.4",
    +   "@storybook/addon-react-native-web": "^0.0.21",
       "@storybook/react-native": "^6.5.4",
    +   "@storybook/react": "^6.5.16",
    +   "@storybook/builder-webpack5": "^6.5.14",
    +   "@storybook/manager-webpack5": "^6.5.14",
    +   "babel-plugin-react-docgen-typescript": "^1.5.1",
    +   "babel-plugin-react-native-web": "^0.18.10",
    +   "metro-react-native-babel-preset": "^0.77.0",
       "babel-loader": "^8.3.0"
     },
     "expo": {
       ...
     },
    + "resolutions": {
    +   "react-docgen-typescript": "2.2.2",
    +   "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.cd77847.0"
    + },
    + "overrides": {
    +   "react-docgen-typescript": "2.2.2",
    +   "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.cd77847.0"
    + },
    }
    
  2. Run npm install

For the web setup, we need to add a few dependencies. Some are required, and some are optional (but recommended for a better experience).

Storybook also allows you to use Vite instead of Webpack, but I didn't try it yet. If you are interested on that kind of setup, check out this link


Refactoring our project structure

After finishing the previous post, you'll have a .storybook folder in the root of your project, containing all the config files for Storybook Native. However, since Storybook Web uses different plugins and addons, we'll need to create separate main.ts and preview.ts files.

So, instead of having this project structure:

/.storybook
-- (Configs for native)
-- stories

We'll have something like this:

/.storybook
-- stories
-- web (Folder for the web setup)
---- main.ts
---- preview.ts
-- native (Folder for the native setup)
---- index.ts (exports the Storybook UI)
---- main.ts
---- preview.ts
---- storybook.requires.js (autogenerated)
  1. Run the following command to create the required folders

     mkdir .storybook/web .storybook/native
    
  2. Move the existing files inside the native folder

     mv .storybook/index.ts .storybook/native/index.ts
     mv .storybook/main.ts .storybook/native/main.ts
     mv .storybook/preview.ts .storybook/native/preview.ts
     mv .storybook/storybook.requires.js .storybook/native/storybook.requires.js
    
  3. In the native folder, we need to update the main.ts file to look into the right paths:

    module.exports = {
    - stories: ["./stories/**/*.stories.?(ts|tsx|js|jsx)"],
    + stories: ["../stories/**/*.stories.?(ts|tsx|js|jsx)"],
     addons: [
       "@storybook/addon-ondevice-controls",
       "@storybook/addon-ondevice-actions",
     ],
    };
    
  4. Create the main.ts and preview.ts files inside the web folder

     touch .storybook/web/main.ts .storybook/web/preview.ts
    

Storybook Web Configurations

Adding config files

As I mentioned earlier, we need a different configuration for Storybook Web.

  1. Let's start by creating the main.ts and preview.ts files inside the .storybook/web directory:

     touch .storybook/web/main.ts
     touch .storybook/web/preview.ts
    
  2. Then, add the following content to the main.ts file:

     module.exports = {
       stories: [
         "../../src/components/**/*.stories.mdx",
         "../../src/components/**/*.stories.@(js|jsx|ts|tsx)",
       ],
       addons: [
         "@storybook/addon-links",
         "@storybook/addon-essentials",
         "@storybook/addon-react-native-web",
       ],
       core: {
         builder: "webpack5",
       },
       framework: "@storybook/react",
     };
    

    Here are a few things to mention about this configuration:

    • It loads stories from the src/components directory with MDX, JS, JSX, TS, and TSX file extensions.

    • Includes essential add-ons, links, and React Native Web support.

    • Includes Webpack 5 setup to be used as the builder and set the framework to Storybook React.

  3. Finally, add the following content to the preview.ts file. Although this file is the same for both Web and Mobile at the moment, it's ok to keep these files separated. That way, we can apply different customizations for each platform as needed.

     export const parameters = {
       actions: { argTypesRegex: "^on[A-Z].*" },
       controls: {
         matchers: {
           color: /(background|color)$/i,
           date: /Date$/,
         },
       },
     };
    

Setup babel to generate docs for TypeScript components

We need to add the babel-plugin-react-docgen-typescript plugin in our babel.config.js file, so we can get useful generated docs for our TypeScript components:

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: [
      "react-native-reanimated/plugin",
      require.resolve("expo-router/babel"),
+     ["babel-plugin-react-docgen-typescript", { exclude: "node_modules" }],
    ],
  };
};

Adding NPM Scripts for Storybook web

Remember how running npm start launches our native app? I wanted to add two more commands: one to run Storybook Web and another to build the web portal. The latter will be useful if you plan to deploy it to Vercel later.

Note that I also had to update the npm start command to point to the new config path. Make sure to check that as well; otherwise, you'll get an error. 🙃

{
  "scripts": {
-   "start": "sb-rn-get-stories && expo start",
+   "start": "sb-rn-get-stories --config-path .storybook/native && expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "storybook-generate": "sb-rn-get-stories",
    "storybook-watch": "sb-rn-watcher",
+   "storybook:web": "start-storybook --config-dir .storybook/web -p 6006",
+   "build-storybook": "build-storybook"
  },
  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
  },
  ...
}

Final touch

Go to the app/index.tsx file and update the require statement to point to the new native folder like this

import React from "react";
import { Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

const storybookEnabled = process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === "true";

const Index = () => {
  return (
    <SafeAreaView>
      <Text>Hello world</Text>
    </SafeAreaView>
  );
};

let EntryPoint = Index;

if (storybookEnabled) {
- const StorybookUI = require("../.storybook").default;
+ const StorybookUI = require("../.storybook/native").default;
  EntryPoint = () => {
    return (
      <View style={{ flex: 1 }}>
        <StorybookUI />
      </View>
    );
  };
}

export default EntryPoint;

Last but not least, add the storybook-static directory to your .gitignore to prevent static files from being included in your source control.

node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
+ storybook-static/

# macOS
.DS_Store

That's it! Now run npm run storybook:web and enjoy!


Recap

In this article, we installed additional dependencies and dealt with Babel configurations to successfully run Storybook web with our React Native components.

I love that everything related to Storybook configuration now resides in the .storybook folder, serving as a single source of truth (including the stories).

Now, you can write your stories once and run Storybook on both native and web platforms. 🪄

Additionally, you can publish your Storybook Web portal to Vercel using the build script.


If you found this article helpful, please let me know in the comments section or hit the like button, so I'll continue writing about this topic.

Happy coding!