DEV Community

Mirco Kraenz
Mirco Kraenz

Posted on

Run Storybook with NX Expo and React Native Paper

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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: {},
};
Enter fullscreen mode Exit fullscreen mode

Running yarn nx storybook PROJECT_NAME should open the browser and you should see the text 'Hi'.

Image

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>;
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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",
        },
      },
    }),
};
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)