Setting Up Storybook Web and Native with Expo Router v2, SDK 49, and TypeScript
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
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" + }, }
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/addon-links (optional)
- This addon can be used to create links that navigate between stories in Storybook.
@storybook/addon-essentials (optional, but highly recommended)
Storybook ships by default with a set of “essential” addons that add to the initial user experience. There are many third-party add-ons as well as “official” add-ons developed by the Storybook core team, such as:
@storybook/addon-react-native-web (Required)
- This addon configures
@storybook/react
to display React Native (RN) projects using React Native for Web (RNW)
- This addon configures
@storybook/builder-webpack5 & @storybook/manager-webpack5 (Required)
- Builder implemented with webpack5 to spin up a dev environment, compile your code into an executable bundle, and update the browser in real-time.
@storybook/react (Required)
- Storybook React renderer for the web
Babel plugins & presets (Required)
babel-plugin-react-docgen-typescript: Plugin to generate docgen data from React components written in TypeScript.
babel-plugin-react-native-web: Plugin that will alias react-native to react-native-web and exclude any modules not required by your app (keeping bundle size down).
metro-react-native-babel-preset: Babel preset for React Native applications. React Native itself uses this Babel preset by default when transforming your app's source code.
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)
Run the following command to create the required folders
mkdir .storybook/web .storybook/native
Move the existing files inside the
native
foldermv .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
In the
native
folder, we need to update themain.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", ], };
Create the
main.ts
andpreview.ts
files inside theweb
foldertouch .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.
Let's start by creating the
main.ts
andpreview.ts
files inside the.storybook/web
directory:touch .storybook/web/main.ts touch .storybook/web/preview.ts
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.
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!