tl;dr: Add vite-plugin-react-native-web to your storybook config in
{projectRoot}/.storybook/main.ts
and you should be good to go. Scroll to end for the full config.
After several days of finally getting this running, while stumbling onto this NX issue several times, I thought I might as well share my solution, and an approach of how to debug if things go haywire (which they did - a lot).
I am assuming you have a working NX workspace with an Expo project and React Native Paper installed.
Outline
- Add Storybook
- Make DOM elements in Storybook work
- Make React Native elements in Storybook work
- Make React Native Paper elements in Storybook work
Add Storybook
Tested under version
@nx/expo@20.1.4
yarn@1.22.19
node@22.11.0
ubuntu 22.04
Let's get started.
yarn nx add @nx/storybook
yarn nx g @nx/react:storybook-configuration PROJECT_NAME
# I answered all questions with the default selection
In my case, this installed
"@nx/storybook": "20.1.4",
"@nx/vite": "20.1.4",
"@nx/web": "20.1.4",
"@storybook/addon-essentials": "^8.2.8",
"@storybook/addon-interactions": "^8.2.8",
"@storybook/core-server": "^8.2.8",
"@storybook/jest": "^0.2.3",
"@storybook/react-vite": "^8.2.8",
"@storybook/test-runner": "^0.13.0",
"@storybook/testing-library": "^0.2.2",
"@vitejs/plugin-react": "^4.2.0",
"storybook": "^8.2.8",
"vite": "^5.0.0"
Depending on your components it should generate at least one *.stories.tsx
file, in my case App.stories.tsx
.
If you start this and it renders perfectly for you. Congratz. You don't need to read further. If you're anything like me, then it doesn't render and here's how I debugged it.
Make DOM elements in Storybook work
I check that at least the most basic setup works by editing App.stories.tsx
and replace the file contents with the most limited I can think of
import { expect } from "@storybook/jest";
import type { Meta, StoryObj } from "@storybook/react";
import { within } from "@storybook/testing-library";
import { App } from "./App";
const XComp = () => <div>Hi</div>;
const meta: Meta<typeof App> = {
component: XComp,
title: "App",
};
export default meta;
type Story = StoryObj<typeof App>;
export const Primary = {
args: {},
};
Running yarn nx storybook PROJECT_NAME
should open the browser and you should see the text 'Hi'.
Make React Native elements in Storybook work
Next up is including something from React Native.
So I change the component definition in App.stories.tsx
to
import { Text } from "react-native";
const MyComponent = () => <Text>Hi</Text>;
This already starts to fail.
The webapp displays an error Failed to fetch dynamically imported module: http://localhost:35021/src/app/App.stories.tsx
while the console shows
✘ [ERROR] Unexpected "typeof"
../../node_modules/react-native/index.js:14:7:
14 │ import typeof ActionSheetIOS from './Libraries/ActionSheetIOS/ActionSheetIOS';
╵ ~~~~~~
9:15:52 PM [vite] error while updating dependencies:
Error: Build failed with 1 error:
../../node_modules/react-native/index.js:14:7: ERROR: Unexpected "typeof"
This is still 'easy' to fix. Because we're rendering for the web, we should use react-native-web instead of react-native. Obviously, I don't want to hardcode the import to react-native-web
as that would not be beneficial for my component library for expo mobile dev. We can however leverage resolve.aliases to import from react-native-web
whenever our code says import ... from 'react-native'
. (I assume this is what happens under the hood in Expo depending on your target platform. Maybe you can teach me or link to some references in the comments. I'd appreciate it!).
Anyway, to resolve the correct import, we go into {projectRoot}/.storybook/main.ts
and replace the config by
const config: StorybookConfig = {
stories: ["../src/app/**/*.@(mdx|stories.@(js|jsx|ts|tsx))"],
addons: ["@storybook/addon-essentials", "@storybook/addon-interactions"],
framework: {
name: "@storybook/react-vite",
options: {},
},
viteFinal: async (config) =>
mergeConfig(config, {
plugins: [react(), nxViteTsPaths()],
resolve: {
alias: {
"react-native": "react-native-web",
},
},
}),
};
Testing again by rerunning yarn nx storybook PROJECT_NAME
should now render the text 'Hi' again. Nice!
So now we have storybook, we can theoretically use regular DOM elements, but more importantly we can use React Native components. A good point to stage and commit our changes. Then it's up to the next level.
Make React Native Paper elements in Storybook work
In my case, I am building on top of React Native Paper, so the natural next step is to change MyComponent
to include
import { PaperProvider, Text } from "react-native-paper";
const MyComponent = () => (
<PaperProvider>
<Text>Hi</Text>
</PaperProvider>
);
This again will fail to render, and the console shows
✘ [ERROR] Expected "from" but found "{"
../../node_modules/react-native-vector-icons/lib/NativeRNVectorIcons.js:3:12:
3 │ import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport';
│ ^
╵ from
✘ [ERROR] The JSX syntax extension is not currently enabled
../../node_modules/react-native-vector-icons/lib/icon-button.js:116:8:
116 │ <TouchableHighlight
╵ ^
The esbuild loader for this file is currently set to "js" but it must be set to "jsx" to be able
to parse JSX syntax. You can use "loader: { '.js': 'jsx' }" to do that.
The problem is that these files are not plain javascript files but include Flow types, and also JSX files.
While the error tells us to customize our loader to treat every js file as jsx, that won't solve the problem with the Flow types in TurboModule. At this point we could probably dive deeper to strip the flow types. But what if we simply solve both problems at once, and the problem of resolving the right import from earlier, too?
Enter vite-plugin-react-native-web!
We install it by running
yarn add -D vite-plugin-react-native-web
and then we go back to {projectRoot}/.storybook/main.ts
and replace the config by
import type { StorybookConfig } from "@storybook/react-vite";
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
import react from "@vitejs/plugin-react";
import { mergeConfig } from "vite";
import reactNativeWeb from "vite-plugin-react-native-web";
const config: StorybookConfig = {
stories: ["../src/app/**/*.@(mdx|stories.@(js|jsx|ts|tsx))"],
addons: ["@storybook/addon-essentials", "@storybook/addon-interactions"],
framework: {
name: "@storybook/react-vite",
options: {},
},
viteFinal: async (config) =>
mergeConfig(config, {
plugins: [react(), nxViteTsPaths(), reactNativeWeb()],
}),
};
export default config;
Notice that we also got rid of the resolve.alias
part. This is because vite-plugin-react-native-web
will take care of that for us.
Now we can rerun yarn nx storybook PROJECT_NAME
and the text 'Hi' should render again. (If you don't get an error but a blank canvas inside storybook, then the theme is probably off. Try switching to dark/light theme in the storybook UI and the text should become visible.)
And that's it! We now have a working storybook setup with React Native Paper components.
Kudos go to the maintainers of vite-plugin-react-native-web
for making this possible.
Top comments (0)