diff --git a/.claude/commands/pr-description.md b/.claude/commands/pr-description.md index 39db98a788..f1749cc774 100644 --- a/.claude/commands/pr-description.md +++ b/.claude/commands/pr-description.md @@ -5,7 +5,7 @@ Generate a pull request description based on the changes in the current branch. ## Instructions 1. First, determine the base branch by checking for a PR or using `next` as default: - ```bash + ```sh gh pr view --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "next" ``` @@ -23,7 +23,7 @@ Generate a pull request description based on the changes in the current branch. - Answer the template questions about examples and documentation 5. Check if a PR exists for this branch: - ```bash + ```sh gh pr view --json number 2>/dev/null ``` diff --git a/CLAUDE.md b/CLAUDE.md index b36a71d531..4a75d8c36c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ This file provides guidance to agents when working with code in this repository. ## Development Commands -```bash +```sh # Initial Setup pnpm install pnpm build diff --git a/MANUAL_SETUP.md b/MANUAL_SETUP.md index 04145b91e7..5e2dbb719a 100644 --- a/MANUAL_SETUP.md +++ b/MANUAL_SETUP.md @@ -1,5 +1,7 @@ # Setup for v8 React Native Storybook +> **This guide is for v8.** For v10 manual setup instructions, see the [manual setup docs](https://storybookjs.github.io/react-native/docs/intro/getting-started/manual-setup). + Before getting into the guide consider using a template for a simpler setup process. **Prebuilt Templates:** @@ -61,7 +63,7 @@ import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], - addons: [], + deviceAddons: [], }; export default main; diff --git a/MIGRATION.md b/MIGRATION.md index 1b0376e243..077365bb81 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,12 +1,15 @@ # Migration - [Migration](#migration) - - [From version 9 to 10](#from-version-9-to-10) - - [Update Storybook dependencies to 10.x](#update-storybook-dependencies-to-10x) + - [From version 10.3 to 10.4](#from-version-103-to-104) - [Update your metro config](#update-your-metro-config) - [Simplify your App.tsx (or Expo Router routes)](#simplify-your-apptsx-or-expo-router-routes) - [Regenerate your requires file](#regenerate-your-requires-file) + - [Move on-device addons to `deviceAddons`](#move-on-device-addons-to-deviceaddons) - [Summary of breaking changes](#summary-of-breaking-changes) + - [Migrate to entry-point swapping](#migrate-to-entry-point-swapping) + - [From version 9 to 10](#from-version-9-to-10) + - [Update Storybook dependencies to 10.x](#update-storybook-dependencies-to-10x) - [From version 8 to 9](#from-version-8-to-9) - [Update Storybook dependencies to 9.x](#update-storybook-dependencies-to-9x) - [Update your `.storybook` folder](#update-your-storybook-folder) @@ -49,36 +52,7 @@ - [Test ids for tabs](#test-ids-for-tabs) - [The server](#the-server) -## From version 9 to 10 - -Version 10 brings Storybook React Native in sync with Storybook core v10, introducing improved Metro configuration and a simplified API. - -### Update Storybook dependencies to 10.x - -You need to update all Storybook dependencies to version 10.x. This includes: - -- `storybook` package (core) -- `@storybook/react` package -- `@storybook/react-native` package -- All `@storybook/addon-ondevice-*` packages - -> Note: You can check the correct version by looking at the `peerDependencies`. Please refer to the [core Storybook migration guide](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#from-version-9x-to-1000) for more details on the breaking changes in Storybook core v10. - -**Example package.json after upgrade:** - -```json -{ - "devDependencies": { - "@storybook/react-native": "^10.0.0", - "@storybook/react": "^10.0.0", - "@storybook/addon-ondevice-controls": "^10.0.0", - "@storybook/addon-ondevice-actions": "^10.0.0", - "@storybook/addon-ondevice-backgrounds": "^10.0.0", - "@storybook/addon-ondevice-notes": "^10.0.0", - "storybook": "^10.0.0" - } -} -``` +## From version 10.3 to 10.4 ### Update your metro config @@ -206,16 +180,40 @@ The metro config (`enabled` flag) automatically handles bundle inclusion: After updating dependencies and configuration, regenerate your `.rnstorybook/storybook.requires.ts` file: -```bash +```sh yarn storybook-generate ``` Or if you have the generate call in your metro config (recommended), just restart metro: -```bash +```sh yarn start --reset-cache ``` +### Move on-device addons to `deviceAddons` + +On-device addons should now be listed in the `deviceAddons` property instead of `addons` in your `.rnstorybook/main.ts`. This prevents errors during server-side operations like `extract`, where Storybook Core would try to evaluate React Native code as Node.js presets. + +**Before:** + +```ts +const main: StorybookConfig = { + stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], + addons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'], +}; +``` + +**After:** + +```ts +const main: StorybookConfig = { + stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], + deviceAddons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'], +}; +``` + +For backwards compatibility, on-device addons in the `addons` array still work — they're detected by the "ondevice" substring in their name. If you're using the Storybook CLI, the `rn-ondevice-addons-to-device-addons` automigration handles this step automatically. + ### Summary of breaking changes 1. **Metro config API changes:** @@ -232,7 +230,59 @@ yarn start --reset-cache 4. **Simplified app entry:** - Custom switcher components no longer needed for bundle optimization - - Metro config handles conditional inclusion automatically + - In-app integration (importing Storybook in App.tsx) continues to work and is fully supported + +5. **`deviceAddons` property:** + - On-device addons should use the new `deviceAddons` property in `main.ts` + - Backwards compatible — `addons` still works for on-device addons + +Version 10.4 introduces entry-point swapping through a bundler-agnostic `withStorybook` wrapper. This lets you switch between your app and Storybook at the bundler entry level, without importing Storybook inside `App.tsx`. + +### Migrate to entry-point swapping + +Instead of importing Storybook in your `App.tsx`, set `STORYBOOK_ENABLED=true` and use the new wrapper: + +```js +// metro.config.js — new recommended import +const { withStorybook } = require('@storybook/react-native/withStorybook'); + +module.exports = withStorybook(config); +``` + +Then run with `STORYBOOK_ENABLED=true expo start`. No changes to `App.tsx` are required. + +This is the recommended approach for new projects. Existing projects can migrate at their own pace — see [Migrating to Entry-Point Swapping](https://storybookjs.github.io/react-native/docs/intro/getting-started/migrating-to-entry-point-swapping) for a step-by-step guide. + +Version 10 brings Storybook React Native in sync with Storybook core v10, introducing improved Metro configuration and a simplified API. + +## From version 9 to 10 + +### Update Storybook dependencies to 10.x + +You need to update all Storybook dependencies to version 10.x. This includes: + +- `storybook` package (core) +- `@storybook/react` package +- `@storybook/react-native` package +- All `@storybook/addon-ondevice-*` packages + +> Note: You can check the correct version by looking at the `peerDependencies`. Please refer to the [core Storybook migration guide](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#from-version-9x-to-1000) for more details on the breaking changes in Storybook core v10. + +**Example package.json after upgrade:** + +```json +{ + "devDependencies": { + "@storybook/react-native": "^10.0.0", + "@storybook/react": "^10.0.0", + "@storybook/addon-ondevice-controls": "^10.0.0", + "@storybook/addon-ondevice-actions": "^10.0.0", + "@storybook/addon-ondevice-backgrounds": "^10.0.0", + "@storybook/addon-ondevice-notes": "^10.0.0", + "storybook": "^10.0.0" + } +} +``` ## From version 8 to 9 @@ -458,7 +508,7 @@ We've removed the types from `@storybook/react-native` and now you should import Heres an example story in version 7: -```typescript +```ts import type { Meta, StoryObj } from '@storybook/react'; import { Button } from './Button'; @@ -482,13 +532,13 @@ export const Basic: Story = { You can now also update main.js to main.ts and use the StorybookConfig type. This is one of the only types we export from @storybook/react-native in this version. -```typescript +```ts // .storybook/main.ts import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], - addons: [ + deviceAddons: [ '@storybook/addon-ondevice-notes', '@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-backgrounds', @@ -501,7 +551,7 @@ export default main; To update preview.js to preview.tsx you can use the Preview type from @storybook/react -```typescript +```ts import type { Preview } from '@storybook/react'; const preview: Preview = { @@ -540,7 +590,7 @@ The setup is the same as `@storybook/addon-react-native-web` but with the reactN #### First install necessary packages -```bash +```sh yarn add -D @storybook/addon-react-native-web @storybook/addon-essentials storybook @storybook/react-webpack5 @storybook/react babel-plugin-react-native-web react-native-web @storybook/addon-react-native-server ``` @@ -557,7 +607,7 @@ With expo you should also add `@expo/metro-runtime`. Add a `main.ts` -```typescript +```ts // .storybook-web/main.ts import type { StorybookConfig } from '@storybook/react-webpack5'; diff --git a/README.md b/README.md index 3c8fbaf083..97084d4625 100644 --- a/README.md +++ b/README.md @@ -57,97 +57,27 @@ Run init to setup your project with all the dependencies and configuration files npm create storybook@latest ``` -The only thing left to do is return Storybook's UI in your app entry point (such as `App.tsx`) like this: - -```tsx -export { default } from './.rnstorybook'; -``` - -Then wrap your metro config with the withStorybook function as seen [below](#additional-steps-update-your-metro-config) - -If you want to be able to swap easily between storybook and your app, have a look at this [blog post](https://dev.to/dannyhw/how-to-swap-between-react-native-storybook-and-your-app-p3o) - -If you want to add everything yourself check out the manual guide [here](https://github.com/storybookjs/react-native/blob/next/MANUAL_SETUP.md). - -#### Additional steps: Update your metro config - -We require the unstable_allowRequireContext transformer option to enable dynamic story imports based on the stories glob in `main.ts`. We can also call the storybook generate function from the metro config to automatically generate the `storybook.requires.ts` file when metro runs. - -**Expo** - -First create metro config file if you don't have it yet. - -```sh -npx expo customize metro.config.js -``` - -Then wrap your config in the withStorybook function as seen below. +Then wrap your bundler config with the `withStorybook` function. It auto-detects Metro vs Re.Pack and handles everything — entry-point swapping, story generation, and optional WebSocket setup. ```js // metro.config.js const { getDefaultConfig } = require('expo/metro-config'); -const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); +const { withStorybook } = require('@storybook/react-native/withStorybook'); const config = getDefaultConfig(__dirname); -// For basic usage with all defaults, this is all you need module.exports = withStorybook(config); - -// Or customize the options -module.exports = withStorybook(config, { - // When false, removes Storybook from bundle (useful for production) - enabled: process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true', - - // Path to your storybook config (default: './.rnstorybook') - configPath: './.rnstorybook', - - // Optional websockets configuration for syncing between devices - // websockets: { - // port: 7007, - // host: 'localhost', - // }, -}); ``` -**React Native** +No changes to `App.tsx` are needed. Set `STORYBOOK_ENABLED=true` and run: -```js -const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); -const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); +```sh +STORYBOOK_ENABLED=true expo start +``` -const defaultConfig = getDefaultConfig(__dirname); +The wrapper automatically swaps your app's entry point with Storybook's entry point. When the variable is not set, your app runs normally with zero Storybook code in the bundle. -/** - * Metro configuration - * https://reactnative.dev/docs/metro - * - * @type {import('metro-config').MetroConfig} - */ -const config = {}; -// set your own config here 👆 - -const finalConfig = mergeConfig(defaultConfig, config); - -// For basic usage with all defaults -module.exports = withStorybook(finalConfig); - -// Or customize the options -module.exports = withStorybook(finalConfig, { - // When false, removes Storybook from bundle (useful for production) - enabled: process.env.STORYBOOK_ENABLED === 'true', - - // Path to your storybook config (default: './.rnstorybook') - configPath: path.resolve(__dirname, './.rnstorybook'), - // note that this is the default so you can the config path blank if you use .rnstorybook - - // Optional websockets configuration for syncing between devices - // Starts a websocket server on the specified port and host on metro start - // websockets: { - // port: 7007, - // host: 'localhost', - // }, -}); -``` +If you want to add everything yourself check out the [manual setup guide](https://storybookjs.github.io/react-native/docs/intro/getting-started/manual-setup). #### Reanimated setup @@ -164,36 +94,9 @@ For projects using [Re.Pack](https://re-pack.dev/) (Rspack/Webpack) instead of M ## Expo router specific setup -```bash -npm create storybook@latest -``` - -choose recommended and then native +For Expo Router projects, you can either use entry-point swapping (recommended) or create a dedicated Storybook route. -```bash -npx expo@latest customize metro.config.js -``` - -copy the metro config - -```js -const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); -module.exports = withStorybook(config); -``` - -add storybook screen to app - -create `app/storybook.tsx` - -```tsx -export { default } from '../.rnstorybook'; -``` - -Then add a way to navigate to your storybook route and I recommend disabling the header for the storybook route. - -Here's a video showing the same setup: - -https://www.youtube.com/watch?v=egBqrYg0AIg +See the full [Expo Router Setup guide](https://storybookjs.github.io/react-native/docs/intro/getting-started/expo-router) for details. ## Writing stories @@ -227,7 +130,7 @@ import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], - addons: [], + deviceAddons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'], }; export default main; @@ -308,7 +211,7 @@ Currently, the addons available are: - [`@storybook/addon-ondevice-notes`](https://storybook.js.org/addons/@storybook/addon-ondevice-notes): Add some Markdown to your stories to help document their usage - [`@storybook/addon-ondevice-backgrounds`](https://storybook.js.org/addons/@storybook/addon-ondevice-backgrounds): change the background of storybook to compare the look of your component against different backgrounds -Install each one you want to use and add them to the `main.ts` addons list as follows: +Install each one you want to use and add them to the `deviceAddons` list in your `main.ts`: ```ts // .rnstorybook/main.ts @@ -316,7 +219,7 @@ import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { // ... rest of config - addons: [ + deviceAddons: [ '@storybook/addon-ondevice-notes', '@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-backgrounds', @@ -327,6 +230,9 @@ const main: StorybookConfig = { export default main; ``` +> [!NOTE] +> `deviceAddons` ensures on-device addons are only loaded at runtime on the device, avoiding errors during server-side operations. For backwards compatibility, listing them in `addons` still works. + ### Using the addons in your story For details of each ondevice addon you can see the readme: @@ -338,32 +244,37 @@ For details of each ondevice addon you can see the readme: ## Hide/Show storybook -In v10, you have flexible options for integrating Storybook into your app: +Starting with v10.4, entry-point swapping is the default setup. Your existing in-app integration setup continues to work and is fully supported, but entry-point swapping is the recommended approach for new projects. -### Option 1: Direct export (simplest) +### Entry-point swapping (recommended, v10.4+) -Just export Storybook directly. Control inclusion via the metro config `enabled` flag: +When using the bundler-agnostic `withStorybook` wrapper, set `STORYBOOK_ENABLED=true` to run Storybook. The wrapper swaps your app's entry point with Storybook's entry point automatically. When the variable is not set, your app runs normally with zero Storybook code in the bundle. -```tsx -// App.tsx -export { default } from './.rnstorybook'; +```json +{ + "scripts": { + "storybook": "STORYBOOK_ENABLED=true expo start", + "storybook:ios": "STORYBOOK_ENABLED=true expo start --ios" + } +} ``` -```js -// metro.config.js -module.exports = withStorybook(config, { - enabled: process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true', -}); +### Expo Router + +Create a dedicated route for Storybook: + +```tsx +// app/storybook.tsx +export { default } from '../.rnstorybook'; ``` -When `enabled: false`, Metro automatically removes Storybook from your bundle. +Then navigate to `/storybook` in your app to view stories. -### Option 2: Conditional rendering +### In-app integration (fully supported) -If you want to switch between your app and Storybook at runtime: +You can also import Storybook directly in your `App.tsx`. This approach continues to work and is fully supported: ```tsx -// App.tsx import StorybookUI from './.rnstorybook'; import { MyApp } from './MyApp'; @@ -374,56 +285,31 @@ export default function App() { } ``` -### Option 3: Expo Router (recommended for Expo) - -Create a dedicated route for Storybook: - -```tsx -// app/storybook.tsx -export { default } from '../.rnstorybook'; -``` - -Then navigate to `/storybook` in your app to view stories. - ## withStorybook wrapper -`withStorybook` is a wrapper function to extend your [Metro config](https://metrobundler.dev/docs/configuration) for Storybook. It accepts your existing Metro config and an object of options for how Storybook should be started and configured. +`withStorybook` is a bundler-agnostic wrapper that configures your project for Storybook. It auto-detects whether you're using Metro or Re.Pack and handles entry-point swapping, story generation, and WebSocket setup. ```js // metro.config.js const { getDefaultConfig } = require('expo/metro-config'); -const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); +const { withStorybook } = require('@storybook/react-native/withStorybook'); const defaultConfig = getDefaultConfig(__dirname); -module.exports = withStorybook(defaultConfig, { - enabled: true, - // See API section below for available options -}); +module.exports = withStorybook(defaultConfig); ``` -### Options - -#### enabled +When `STORYBOOK_ENABLED=true` is set, the wrapper activates. When it's not set, the wrapper is a no-op and your app runs normally. -Type: `boolean`, default: `true` - -Controls whether Storybook is included in your app bundle. When `true`, enables Storybook metro configuration and generates the `storybook.requires` file. When `false`, removes all Storybook code from the bundle by replacing imports with empty modules. +### Options -This is useful for conditionally including Storybook in development but excluding it from production builds: +Options can be passed as a second argument. Most settings can also be controlled via environment variables (see [Environment Variables](https://storybookjs.github.io/react-native/docs/intro/configuration/environment-variables)). -```js -// metro.config.js -const { getDefaultConfig } = require('expo/metro-config'); -const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); +#### configPath -const defaultConfig = getDefaultConfig(__dirname); +Type: `string`, default: `path.resolve(process.cwd(), './.rnstorybook')` -module.exports = withStorybook(defaultConfig, { - enabled: process.env.STORYBOOK_ENABLED === 'true', - // ... other options -}); -``` +The location of your Storybook configuration directory, which includes `main.ts` and other project-related files. #### useJs @@ -431,12 +317,6 @@ Type: `boolean`, default: `false` Generates the `.rnstorybook/storybook.requires` file in JavaScript instead of TypeScript. -#### configPath - -Type: `string`, default: `path.resolve(process.cwd(), './.rnstorybook')` - -The location of your Storybook configuration directory, which includes `main.ts` and other project-related files. - #### docTools Type: `boolean`, default: `true` @@ -447,7 +327,7 @@ Whether to include doc tools in the storybook.requires file. Doc tools provide a Type: `boolean`, default: `false` -Whether to use lite mode for Storybook. In lite mode, the default Storybook UI is mocked out so you don't need to install all its dependencies like react-native-reanimated. This is useful for reducing bundle size and dependencies. Use this when using @storybook/react-native-ui-lite instead of @storybook/react-native-ui. +Whether to use lite mode for Storybook. In lite mode, the default Storybook UI is mocked out so you don't need to install all its dependencies like react-native-reanimated. This is useful for reducing bundle size and dependencies. Use this when using @storybook/react-native-ui-lite instead of @storybook/react-native-ui. Note: `STORYBOOK_DISABLE_UI=true` is equivalent to `onDeviceUI: false`, not `liteMode: true`. #### experimental_mcp @@ -455,50 +335,21 @@ Type: `boolean`, default: `false` Enables an experimental MCP (Model Context Protocol) server for AI tooling to query Storybook documentation and component/story metadata. -You can enable MCP with or without websockets: - -- `experimental_mcp: true` starts the HTTP MCP endpoint -- adding `websockets` also enables story selection tools over the same channel server - The MCP server is available at the `/mcp` endpoint on the Storybook channel server. Configure your MCP client via its settings UI, or use: ```sh npx mcp-add --type http --url "http://localhost:7007/mcp" --scope project ``` -You'll need to adjust the URL accordingly if you're using non-default `websockets.(host|port|secured)` [properties](#websockets). - ### websockets Type: `'auto' | { host?: string, port?: number, secured?: boolean, key?: string | Buffer, cert?: string | Buffer, ca?: string | Buffer | Array, passphrase?: string }`, default: `undefined` If specified, create a WebSocket server on startup. This allows you to sync up multiple devices to show the same story and [arg](https://storybook.js.org/docs/writing-stories/args) values connected to the story in the UI. -Use `'auto'` to automatically detect your LAN IP and inject host/port into the generated `storybook.requires` file. - -### websockets.host +Use `'auto'` to automatically detect your LAN IP and inject host/port into the generated `storybook.requires` file. WebSocket settings can also be overridden via `STORYBOOK_WS_HOST`, `STORYBOOK_WS_PORT`, and `STORYBOOK_WS_SECURED` environment variables. -Type: `string`, default: `'localhost'` - -The host on which to run the WebSocket, if specified. - -### websockets.port - -Type: `number`, default: `7007` - -The port on which to run the WebSocket, if specified. - -### websockets.secured - -Type: `boolean`, default: `false` - -When `true`, the channel server starts on `https` and upgrades WebSocket clients over `wss`. - -### websockets.key / websockets.cert - -Type: `string | Buffer`, default: `undefined` - -TLS private key and certificate used when `secured` is `true`. +> **Note:** A Metro-specific `withStorybook` is also available at `@storybook/react-native/metro/withStorybook` for advanced Metro configuration. See the [Metro Configuration docs](https://storybookjs.github.io/react-native/docs/intro/configuration/metro-configuration) for details. ## getStorybookUI options @@ -506,32 +357,19 @@ You can pass these parameters to getStorybookUI call in your storybook entry poi ```ts { - // initialize storybook with a specific story. eg: `mybutton--largebutton` or `{ kind: 'MyButton', name: 'LargeButton' }` initialSelection?: string | Object; - // Custom storage to be used instead of AsyncStorage storage?: { getItem: (key: string) => Promise; setItem: (key: string, value: string) => Promise; }; - // show the onDevice UI onDeviceUI?: boolean; - // enable websockets for the Storybook UI - enableWebsockets?: boolean; - // query params for the websocket connection - query?: string; - // host for the websocket connection - host?: string; - // port for the websocket connection - port?: number; - // use secured websockets - secured?: boolean; - // store the last selected story in the device's storage shouldPersistSelection?: boolean; - // theme for the Storybook UI theme: Partial; } ``` +> **Note:** WebSocket options (`enableWebsockets`, `host`, `port`, `secured`) are auto-injected when using the bundler-agnostic `withStorybook` wrapper. You only need to set them manually if you're using the Metro-specific wrapper or a custom setup. + ## Feature Flags Feature flags let you opt into new functionality without breaking existing behavior. In the next major version, the behavior behind these flags will become the default and the flags will no longer be needed. @@ -544,7 +382,7 @@ import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], - addons: ['@storybook/addon-ondevice-controls'], + deviceAddons: ['@storybook/addon-ondevice-controls'], features: { ondeviceBackgrounds: true, }, diff --git a/docs/blog/2026-02-18-storybook-v10.mdx b/docs/blog/2026-02-18-storybook-v10.mdx index 916446cf35..f5d9d8a404 100644 --- a/docs/blog/2026-02-18-storybook-v10.mdx +++ b/docs/blog/2026-02-18-storybook-v10.mdx @@ -82,7 +82,7 @@ Enable it via a feature flag in `main.ts`: ```ts const main: StorybookConfig = { stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], - addons: ['@storybook/addon-ondevice-controls'], + deviceAddons: ['@storybook/addon-ondevice-controls'], features: { ondeviceBackgrounds: true, }, @@ -95,7 +95,7 @@ Backgrounds are configured in `preview.tsx` using the standard Storybook paramet We've published agent skills that guide AI coding assistants through setting up and working with React Native Storybook. If you use Claude Code, Cursor, Windsurf, or any agent that supports skills, you can install them with: -```bash +```sh npx skills add storybookjs/react-native ``` diff --git a/docs/docs/intro/addons/controls.md b/docs/docs/intro/addons/controls.md index 5c14a6f277..e6db7643f2 100644 --- a/docs/docs/intro/addons/controls.md +++ b/docs/docs/intro/addons/controls.md @@ -8,17 +8,17 @@ The `@storybook/addon-ondevice-controls` addon provides interactive controls for ## Installation -```bash +```sh npm install @storybook/addon-ondevice-controls ``` -Add it to your addons list in `.rnstorybook/main.ts`: +Add it to your `deviceAddons` list in `.rnstorybook/main.ts`: -```typescript +```ts const main: StorybookConfig = { - addons: [ + deviceAddons: [ '@storybook/addon-ondevice-controls', - // ... other addons + // ... other on-device addons ], }; ``` @@ -33,7 +33,7 @@ The React Native controls addon supports **13 different control types** optimize Basic text input control for string values. -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -48,7 +48,7 @@ export default { Numeric input with optional range slider functionality. -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -75,7 +75,7 @@ export default { Dedicated slider control for numeric ranges. -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -97,7 +97,7 @@ export default { Toggle switch using React Native's native Switch component. -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -114,7 +114,7 @@ export default { Dropdown selection with support for custom labels. -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -143,7 +143,7 @@ export default { Radio button selection for exclusive choices. -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -159,7 +159,7 @@ export default { Inline radio button layout for horizontal display. -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -177,7 +177,7 @@ export default { Multiple selection control returning an array of values. -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -195,7 +195,7 @@ export default { Full-featured color picker with mobile-optimized interface. -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -215,7 +215,7 @@ export default { Date and time picker with native mobile interfaces. -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -234,7 +234,7 @@ export default { Array input control that parses comma-separated values. -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -252,7 +252,7 @@ export default { JSON object editor with syntax validation. -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -270,7 +270,7 @@ export default { Controls can be automatically inferred from your component's prop types: -```typescript +```ts // TypeScript component interface ButtonProps { label: string; // → text control @@ -290,7 +290,7 @@ export default { ### Complete Story with Multiple Control Types -```typescript +```ts import type { Meta, StoryObj } from '@storybook/react-native'; import { MyComponent } from './MyComponent'; @@ -369,7 +369,7 @@ export const Interactive: Story = { ### Conditional Controls -```typescript +```ts export default { component: MyComponent, argTypes: { @@ -402,7 +402,7 @@ Some controls require additional React Native dependencies: Install these as needed: -```bash +```sh npm install @react-native-community/slider @react-native-community/datetimepicker ``` diff --git a/docs/docs/intro/addons/index.md b/docs/docs/intro/addons/index.md index d03b69f6e1..305ba3f776 100644 --- a/docs/docs/intro/addons/index.md +++ b/docs/docs/intro/addons/index.md @@ -13,13 +13,14 @@ The addons made available by us are the following. There are more addons availab ## Configuration -To use these addons, add them to your `.storybook/main.ts`: +To use on-device addons, add them to the `deviceAddons` property in your `.rnstorybook/main.ts`: ```ts import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { - addons: [ + stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], + deviceAddons: [ '@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-backgrounds', '@storybook/addon-ondevice-actions', @@ -30,6 +31,12 @@ const main: StorybookConfig = { export default main; ``` +:::info Why `deviceAddons`? +On-device addons contain React Native code that can only run on the device. When listed in the regular `addons` array, Storybook Core tries to evaluate them as presets during server-side operations (like `extract` or `build`), which fails because Node.js can't load React Native modules. The `deviceAddons` property ensures they're only loaded at runtime on the device. + +For backwards compatibility, on-device addons in the `addons` array still work — they're detected by the "ondevice" substring in their name and handled correctly. However, `deviceAddons` is the recommended approach. +::: + ## Actions The Actions addon lets you log events and actions inside your stories. It's useful for verifying component interactions and event handling. diff --git a/docs/docs/intro/configuration/backgrounds.md b/docs/docs/intro/configuration/backgrounds.md index 4fb81c837c..e6ad8d611a 100644 --- a/docs/docs/intro/configuration/backgrounds.md +++ b/docs/docs/intro/configuration/backgrounds.md @@ -8,7 +8,7 @@ The `ondeviceBackgrounds` [feature flag](./feature-flags.md) enables the new bac This flag was introduced in v10.3 as a non-breaking way to opt into the new syntax. **In the next major version this will be the default behavior and the flag will no longer be needed.** -When this flag is enabled you do **not** need to install `@storybook/addon-ondevice-backgrounds` or add it to your `addons` array. +When this flag is enabled you do **not** need to install `@storybook/addon-ondevice-backgrounds` or add it to your `deviceAddons` array. ## Setup @@ -20,7 +20,7 @@ import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], - addons: ['@storybook/addon-ondevice-controls'], + deviceAddons: ['@storybook/addon-ondevice-controls'], features: { ondeviceBackgrounds: true, }, @@ -56,15 +56,18 @@ export default preview; - `parameters.backgrounds.options` — an object keyed by identifier. Each entry has a `name` (display label) and `value` (hex color). - `initialGlobals.backgrounds.value` — sets the initially selected background by its key. -### 3. Regenerate the requires file +After changing `main.ts`, restart your bundler so the flag takes effect. The `withStorybook` wrapper regenerates `storybook.requires.ts` automatically on start. -After changing `main.ts`, regenerate `storybook.requires.ts` so the flag takes effect: +
+Not using a withStorybook wrapper? + +If you're not using the `withStorybook` wrapper, regenerate the requires file manually: ```sh -npm run storybook-generate +npx sb-rn-get-stories ``` -Or restart Metro, which regenerates the file automatically. +
## Overriding at the story level diff --git a/docs/docs/intro/configuration/cli-configuration.md b/docs/docs/intro/configuration/cli-configuration.md index 030ef69763..dff337d745 100644 --- a/docs/docs/intro/configuration/cli-configuration.md +++ b/docs/docs/intro/configuration/cli-configuration.md @@ -4,21 +4,21 @@ sidebar_position: 5 # CLI Configuration -React Native Storybook provides CLI commands to help with setup and story generation. This page covers all available commands and their options. +:::info You probably don't need this +If you're using the `withStorybook` bundler wrapper (the recommended setup), the `storybook.requires.ts` file is generated and updated automatically every time your bundler starts. You don't need to run any CLI commands for story generation. -## Installation +The `sb-rn-get-stories` command documented here is only needed if you've chosen **not** to use a `withStorybook` wrapper at all — for example, in a fully custom build pipeline. +::: -The CLI is included when you install `@storybook/react-native` +React Native Storybook provides the `sb-rn-get-stories` CLI command for manual story generation. The CLI is included when you install `@storybook/react-native`. -## Available Commands - -### `sb-rn-get-stories` +## `sb-rn-get-stories` Generates the `storybook.requires.ts` file that imports all your stories and configurations. #### Basic Usage -```bash +```sh # Generate with default options npx sb-rn-get-stories @@ -39,7 +39,7 @@ Specify the path to your Storybook configuration folder. - **Default**: `./.rnstorybook` - **Type**: string -```bash +```sh # Custom config location npx sb-rn-get-stories --config-path ./.storybook npx sb-rn-get-stories -c ./src/storybook @@ -52,7 +52,7 @@ Generate JavaScript files instead of TypeScript. - **Default**: `false` (generates TypeScript) - **Type**: boolean -```bash +```sh # Generate storybook.requires.js instead of .ts npx sb-rn-get-stories --use-js npx sb-rn-get-stories -js @@ -67,7 +67,7 @@ Exclude documentation tools from the generated file. - **Default**: includes doc tools - **Type**: boolean -```bash +```sh # Exclude doc tools to reduce bundle size npx sb-rn-get-stories --no-doc-tools npx sb-rn-get-stories -D @@ -79,14 +79,14 @@ npx sb-rn-get-stories -D Display help information. -```bash +```sh npx sb-rn-get-stories --help npx sb-rn-get-stories -h ``` #### Complete Example -```bash +```sh # Generate with all options npx sb-rn-get-stories \ --config-path ./src/.storybook \ @@ -104,12 +104,12 @@ The CLI looks for these files in your config path: Defines stories location and addons: -```typescript +```ts import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], - addons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'], + deviceAddons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'], }; export default main; @@ -119,7 +119,7 @@ export default main; Global decorators and parameters: -```typescript +```ts import { Preview } from '@storybook/react-native'; const preview: Preview = { @@ -147,25 +147,17 @@ Auto-generated file containing: **Important**: Never edit this file manually - it's regenerated automatically. -## Integration with Build Tools - -### Metro Integration +## CLI usage requirements -The `withStorybook` Metro wrapper automatically runs story generation: +### With a `withStorybook` wrapper (you don't need the CLI) -```js -// metro.config.js -const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); +Both the bundler-agnostic wrapper (`@storybook/react-native/withStorybook`) and the Metro-specific wrapper (`@storybook/react-native/metro/withStorybook`) automatically generate and update `storybook.requires.ts` on every bundler start. If you're using either wrapper, you can skip the CLI entirely. -module.exports = withStorybook(config, { - configPath: './.rnstorybook', // Must match CLI --config-path - useJs: false, // Must match CLI --use-js -}); -``` +If you've customized `configPath` or `useJs` in your wrapper, the generated file will respect those options automatically. -### Manual Integration +### Without a wrapper (you need the CLI) -For custom build setups, run the CLI before building: +If you're not using a `withStorybook` wrapper — for example, in a fully custom build pipeline — run `sb-rn-get-stories` manually whenever you add, remove, or rename story files: ```json { @@ -186,7 +178,7 @@ Doc tools enable automatic extraction of component props to generate controls. T 1. **Install the babel plugin**: -```bash +```sh npm install --save-dev babel-plugin-react-docgen-typescript ``` @@ -202,7 +194,7 @@ module.exports = { 3. **Ensure doc tools are enabled** (default behavior): -```bash +```sh npx sb-rn-get-stories # Doc tools included by default ``` @@ -215,7 +207,7 @@ Disable doc tools to reduce bundle size when: - Not using automatic controls - Manually defining all argTypes -```bash +```sh npx sb-rn-get-stories --no-doc-tools ``` @@ -238,14 +230,14 @@ npx sb-rn-get-stories --no-doc-tools 1. **Check config path**: -```bash +```sh # Verify path exists ls -la ./.rnstorybook ``` 2. **Check story patterns**: -```typescript +```ts // main.ts - ensure patterns match your file structure stories: [ '../components/**/*.stories.tsx', // More specific @@ -255,7 +247,7 @@ stories: [ 3. **Clear Metro cache**: -```bash +```sh npx react-native start --reset-cache ``` @@ -268,12 +260,4 @@ npx react-native start --reset-cache ## Best Practices 1. **Add to git**: Commit the generated `storybook.requires.ts` file -2. **Consistent configuration**: Ensure CLI options match Metro wrapper options: - -```js -// metro.config.js -module.exports = withStorybook(config, { - configPath: './.storybook', // Matches: sb-rn-get-stories -c ./.storybook - useJs: true, // Matches: sb-rn-get-stories --use-js -}); -``` +2. **Consistent configuration**: If you use the CLI alongside a `withStorybook` wrapper, ensure the CLI options match the wrapper options (`configPath`, `useJs`, `docTools`) diff --git a/docs/docs/intro/configuration/environment-variables.md b/docs/docs/intro/configuration/environment-variables.md new file mode 100644 index 0000000000..c3a23f3fe4 --- /dev/null +++ b/docs/docs/intro/configuration/environment-variables.md @@ -0,0 +1,133 @@ +--- +sidebar_position: 7 +description: Reference for all environment variables supported by Storybook for React Native. +keywords: [react native, storybook, environment variables, configuration, STORYBOOK_ENABLED] +--- + +# Environment Variables + +The bundler-agnostic `withStorybook` wrapper reads configuration from environment variables at build time. This lets you control Storybook behavior without changing code. + +## Reference + +### `STORYBOOK_ENABLED` + +Controls whether Storybook is active. When set to `true`, the wrapper swaps your app's entry point with Storybook's entry point and includes all Storybook code in the bundle. When unset or `false`, the wrapper is a no-op and your app runs normally with no Storybook code. + +- **Type:** `'true' | 'false'` +- **Default:** `false` +- **Used by:** `withStorybook` (bundler-agnostic wrapper) + +```sh +STORYBOOK_ENABLED=true expo start +``` + +### `STORYBOOK_WS_HOST` + +Sets the WebSocket server hostname. Overrides any `websockets.host` value passed in the config options. + +- **Type:** `string` +- **Default:** value from config options, or `undefined` +- **Used by:** `withStorybook` WebSocket configuration + +`auto` is **not** a valid value for `STORYBOOK_WS_HOST`. Use an explicit hostname or IP address. + +```sh +STORYBOOK_WS_HOST=192.168.1.100 expo start +``` + +### `STORYBOOK_WS_PORT` + +Sets the WebSocket server port. Overrides any `websockets.port` value passed in the config options. + +- **Type:** `string` (parsed as integer) +- **Default:** value from config options, or `7007` +- **Used by:** `withStorybook` WebSocket configuration + +```sh +STORYBOOK_WS_PORT=8080 expo start +``` + +### `STORYBOOK_WS_SECURED` + +Enables TLS for the WebSocket server (`wss://` instead of `ws://`) and HTTPS for the channel server. + +- **Type:** `'true' | 'false'` +- **Default:** `false` +- **Used by:** `withStorybook` WebSocket configuration + +```sh +STORYBOOK_WS_SECURED=true expo start +``` + +:::note +When using secured WebSockets, you also need to provide TLS certificates via the config options (`websockets.key`, `websockets.cert`). These cannot be set via environment variables. +::: + +### `STORYBOOK_SERVER` + +Controls whether the channel server starts alongside your app. The channel server handles WebSocket connections and (optionally) the MCP endpoint. + +- **Type:** `'true' | 'false'` +- **Default:** `true` +- **Used by:** `withStorybook` server configuration + +### `STORYBOOK_DISABLE_UI` + +Enables lite mode, which mocks out the default Storybook UI dependencies (like `react-native-reanimated`). Use this when you want a lighter bundle, for example with `@storybook/react-native-ui-lite`. + +- **Type:** `'true' | 'false'` +- **Default:** `false` +- **Used by:** `withStorybook` UI configuration + +## Usage patterns + +### Basic Storybook startup + +```sh +STORYBOOK_ENABLED=true expo start +``` + +### With WebSocket auto-detection + +If your metro/bundler config has `websockets: 'auto'`, you only need `STORYBOOK_ENABLED`: + +```sh +STORYBOOK_ENABLED=true expo start +``` + +The LAN IP and port are detected automatically. + +### Override WebSocket host for physical devices + +```sh +STORYBOOK_ENABLED=true STORYBOOK_WS_HOST=192.168.1.100 STORYBOOK_WS_PORT=7007 expo start +``` + +### Combining in package.json scripts + +```json +{ + "scripts": { + "storybook": "STORYBOOK_ENABLED=true expo start", + "storybook:device": "STORYBOOK_ENABLED=true STORYBOOK_WS_HOST=192.168.1.100 expo start" + } +} +``` + +:::note Windows +Use `cross-env` to set environment variables on Windows: + +```json +{ + "scripts": { + "storybook": "cross-env STORYBOOK_ENABLED=true expo start" + } +} +``` + +::: + +## Precedence + +Environment variables always take precedence over values passed in the `withStorybook` config options. This lets you set defaults in your config and override them per-run from the command line. diff --git a/docs/docs/intro/configuration/feature-flags.md b/docs/docs/intro/configuration/feature-flags.md index ead0219dbd..d27d70bfd9 100644 --- a/docs/docs/intro/configuration/feature-flags.md +++ b/docs/docs/intro/configuration/feature-flags.md @@ -18,7 +18,7 @@ import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], - addons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'], + deviceAddons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'], features: { ondeviceBackgrounds: true, }, diff --git a/docs/docs/intro/configuration/index.md b/docs/docs/intro/configuration/index.md index 1c7f87a3d5..b6024a7fcc 100644 --- a/docs/docs/intro/configuration/index.md +++ b/docs/docs/intro/configuration/index.md @@ -17,9 +17,9 @@ The storybook configuration consists of several key files: ### main.ts -The `main.ts` file is your primary configuration entry point, located in the `.storybook` directory. +The `main.ts` file is your primary configuration entry point, located in the `.rnstorybook` directory. -```typescript +```ts import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { @@ -35,7 +35,7 @@ const main: StorybookConfig = { }, ], - addons: [ + deviceAddons: [ '@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-backgrounds', '@storybook/addon-ondevice-actions', @@ -52,14 +52,15 @@ export default main; - `directory`: Base directory for stories - `titlePrefix`: Optional prefix for story titles - `files`: Glob pattern for story files -- `addons`: Array of addon packages to include +- `deviceAddons`: Array of on-device addon packages (recommended for `@storybook/addon-ondevice-*` packages). These are loaded only at runtime on the device and are not evaluated as presets by Storybook Core, avoiding errors during server-side operations like `extract`. +- `addons`: Array of addon packages evaluated as Storybook presets. On-device addons listed here still work for backwards compatibility, but `deviceAddons` is preferred. - `features`: Enable new functionality (see [Feature Flags](./feature-flags.md)) ## preview.tsx The `preview.tsx` file configures the story rendering environment and global parameters. -```typescript +```ts import { Preview } from '@storybook/react-native'; import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds'; @@ -117,7 +118,7 @@ Other than story sort the other parameters can be overwritten per story. The entry point file configures the Storybook UI and runtime behavior. -```typescript +```ts import { view } from './storybook.requires'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -143,10 +144,11 @@ const StorybookUIRoot = view.getStorybookUI({ setItem: AsyncStorage.setItem, }, - // Websocket Options - enableWebsockets: false, - host: 'localhost', - port: 7007, + // Websocket Options (auto-injected when using the bundler-agnostic withStorybook) + // You only need these if you're NOT using the bundler-agnostic wrapper: + // enableWebsockets: false, + // host: 'localhost', + // port: 7007, // CustomUIComponent: MyCustomUI, // Optional custom UI component }); @@ -173,7 +175,7 @@ export default StorybookUIRoot; - `getItem`: Function to retrieve stored values - `setItem`: Function to store values -- **Websocket Options** +- **Websocket Options** (only needed for manual setups. These are auto-injected when using the bundler-agnostic `withStorybook` wrapper) - `enableWebsockets`: Enable remote control (default: false) - `host`: Websocket host (default: 'localhost') - `port`: Websocket port (default: 7007) @@ -181,19 +183,21 @@ export default StorybookUIRoot; - **Custom UI Options** - `CustomUIComponent`: Replace the default Storybook UI with your own implementation -## Metro Configuration +## Bundler Configuration -Wrap your Metro config with the `withStorybook` function: +Wrap your bundler config with the `withStorybook` function. This wrapper auto-detects whether you're using Metro or Re.Pack: ```js const { getDefaultConfig } = require('expo/metro-config'); -const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); +const { withStorybook } = require('@storybook/react-native/withStorybook'); const config = getDefaultConfig(__dirname); module.exports = withStorybook(config); ``` -For detailed Metro configuration options, see the [Metro Configuration guide](./metro-configuration.md). +When `STORYBOOK_ENABLED=true` is set, the wrapper automatically swaps your app's entry point with Storybook's entry point, generates the `storybook.requires` file, and configures WebSocket connections. See [Environment Variables](./environment-variables.md) for all supported variables. + +For Metro-specific options (advanced), see the [Metro Configuration guide](./metro-configuration.md). ## Generated Files @@ -236,6 +240,10 @@ The new globals-based backgrounds API with full-screen support, available now be Enable remote control of Storybook from external tools, browsers, or other devices for testing and automation. +### [Environment Variables](./environment-variables.md) + +Reference for all environment variables supported by the `withStorybook` wrapper, including `STORYBOOK_ENABLED`, WebSocket overrides, and more. + ### [MCP Configuration](./mcp-configuration.md) Enable the experimental MCP endpoint (`/mcp`) for AI tooling to query Storybook docs and metadata (available from v10.3). diff --git a/docs/docs/intro/configuration/metro-configuration.md b/docs/docs/intro/configuration/metro-configuration.md index 9af6fce94b..2cf624dba8 100644 --- a/docs/docs/intro/configuration/metro-configuration.md +++ b/docs/docs/intro/configuration/metro-configuration.md @@ -4,185 +4,147 @@ sidebar_position: 3 # Metro Configuration -The `withStorybook` function is a Metro configuration wrapper that enables Storybook functionality in your React Native app. It handles automatic story discovery, file generation, and optional WebSocket server setup. +This page is the configuration reference for both `withStorybook` wrappers available for Metro projects. For initial setup, see [Getting Started](../getting-started/index.md) (entry-point swapping) or [Manual Setup](../getting-started/manual-setup.md) (in-app integration). -## Basic Setup +## Bundler-agnostic wrapper (recommended) ```js +const { withStorybook } = require('@storybook/react-native/withStorybook'); +``` + +This is the recommended wrapper for new projects (v10.4+). It auto-detects Metro vs Re.Pack, performs entry-point swapping when `STORYBOOK_ENABLED=true` is set, and is a strict no-op otherwise — zero Storybook code in the bundle. + +### Basic usage + +```js +// metro.config.js const { getDefaultConfig } = require('expo/metro-config'); -const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); +const { withStorybook } = require('@storybook/react-native/withStorybook'); const config = getDefaultConfig(__dirname); module.exports = withStorybook(config); ``` -The wrapper works with sensible defaults, so you can use it without any options. +Configuration is driven by environment variables. See [Environment Variables](./environment-variables.md) for the full reference. -## Configuration Options +### Options -### Complete Options Reference +The bundler-agnostic wrapper accepts an optional second argument for options that can't be expressed as env vars: ```js module.exports = withStorybook(config, { - // Enable/disable Storybook functionality - defaults to true - enabled: true, + configPath: './.rnstorybook', // Storybook config directory (default: './.rnstorybook') + useJs: false, // Generate .js instead of .ts (default: false) + docTools: true, // Auto arg extraction (default: true) + websockets: 'auto', // 'auto' detects LAN IP, or { host, port } + experimental_mcp: false, // Enable MCP endpoint at /mcp (default: false) +}); +``` - // Path to your Storybook configuration folder - defaults to './.rnstorybook' - configPath: './.rnstorybook', +Environment variables (`STORYBOOK_ENABLED`, `STORYBOOK_WS_HOST`, etc.) always take precedence over options passed here. - // Use JavaScript instead of TypeScript for generated files - defaults to false - useJs: false, +## Metro-specific wrapper - // Include doc tools for automatic args - defaults to true - docTools: true, +```js +const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); +``` + +Use this wrapper for [in-app integration](../getting-started/manual-setup.md) or when you need direct control over Metro behavior (e.g. the `enabled` option). This wrapper does **not** do entry-point swapping — your app remains the entry point and you control where Storybook renders. + +### Basic usage + +```js +// metro.config.js +const { getDefaultConfig } = require('expo/metro-config'); +const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); + +const config = getDefaultConfig(__dirname); - // Use lite mode (mocks out default Storybook UI dependencies) - defaults to false - liteMode: false, +module.exports = withStorybook(config, { + enabled: process.env.STORYBOOK_ENABLED === 'true', +}); +``` - // WebSocket server configuration - defaults to undefined - // Use 'auto' to detect LAN IP and inject host/port into storybook.requires - // You can also use { host, port }. 'auto' is available from v10.2. - websockets: 'auto', +### Complete options reference - // Enable experimental MCP endpoint (/mcp) - defaults to false - experimental_mcp: false, +```js +module.exports = withStorybook(config, { + enabled: true, // Include Storybook in the bundle (default: true) + configPath: './.rnstorybook', // Storybook config directory (default: './.rnstorybook') + useJs: false, // Generate .js instead of .ts (default: false) + docTools: true, // Auto arg extraction (default: true) + liteMode: false, // Mock default UI deps (default: false) + websockets: 'auto', // WebSocket config (default: undefined) + experimental_mcp: false, // Enable MCP endpoint (default: false) }); ``` -### Option Details +### Option details -#### `enabled` (boolean) +#### `enabled` - **Default**: `true` -- **Purpose**: Controls whether Storybook is included in your app bundle -- **Behavior**: - - When `true`: Enables Storybook metro configuration and generates `storybook.requires` file - - When `false`: Removes all Storybook code from the bundle by replacing imports with empty modules +- Controls whether Storybook is included in your app bundle. When `false`, all `@storybook/*` and `storybook/*` imports are replaced with empty modules, and your Storybook config directory imports are stubbed out. If you try to render Storybook when disabled, you'll get a blank screen with a warning. -#### `configPath` (string) +#### `configPath` - **Default**: `'./.rnstorybook'` -- **Purpose**: Path to your Storybook configuration directory -- **Example**: `'./storybook'` or `'./src/.storybook'` +- Path to your Storybook configuration directory (containing `main.ts`, `preview.tsx`, etc.). -#### `useJs` (boolean) +#### `useJs` - **Default**: `false` -- **Purpose**: Generate JavaScript files instead of TypeScript -- **Note**: Useful for projects not using TypeScript. Generates `storybook.requires.js` instead of `storybook.requires.ts` +- Generate `storybook.requires.js` instead of `storybook.requires.ts`. Useful for projects not using TypeScript. -#### `docTools` (boolean) +#### `docTools` - **Default**: `true` -- **Purpose**: Include utilities for automatic arg extraction -- **Related**: Works with `babel-plugin-react-docgen-typescript` +- Include utilities for automatic arg extraction. Works with `babel-plugin-react-docgen-typescript`. -#### `liteMode` (boolean) +#### `liteMode` - **Default**: `false` -- **Purpose**: Use lite mode to reduce bundle size by mocking out the default Storybook UI -- **Benefits**: Removes dependencies like react-native-reanimated, reducing app bundle size -- **Note**: Only affects the on-device UI components from `@storybook/react-native-ui` - -Use this when using @storybook/react-native-ui-lite instead of @storybook/react-native-ui. +- Mocks out the default Storybook UI dependencies (like `react-native-reanimated`), reducing bundle size. Use when pairing with `@storybook/react-native-ui-lite` instead of `@storybook/react-native-ui`. -#### `websockets` (`'auto' | object`) +#### `websockets` -- **Default**: `undefined` -- **Purpose**: Configure WebSocket server for remote control -- **Properties**: - - `port`: WebSocket server port number - - `host`: WebSocket server hostname -- **Manual mode**: - - You can always pass `{ host, port }` explicitly. -- **Special value**: - - `'auto'`: Detects LAN IP automatically and injects host/port into generated `storybook.requires` (available from `v10.2`) -- **Requirements**: Make sure you use the same port in the getStorybookUI configuration. On android you must use your machine's IP address instead of `localhost` if running on a physical device. +- **Default**: `undefined` (disabled) +- Configure the WebSocket server for remote control and syncing. +- `'auto'`: Detects your LAN IP automatically and injects host/port into the generated `storybook.requires` file (available from v10.2). +- `{ host, port }`: Manual configuration. On Android physical devices, use your machine's IP instead of `localhost`. -#### `experimental_mcp` (boolean) +#### `experimental_mcp` - **Default**: `false` -- **Purpose**: Enables an experimental MCP (Model Context Protocol) endpoint at `/mcp` on the Storybook channel server -- **Available from**: `v10.3` -- **Behavior**: - - Can run without websockets for MCP documentation/query tooling - - Story selection MCP tools require `websockets` to be enabled -- **Related**: See [MCP Configuration](./mcp-configuration.md) +- Enables an experimental MCP (Model Context Protocol) endpoint at `/mcp` on the Storybook channel server (available from v10.3). Story selection tools require `websockets` to be enabled. See [MCP Configuration](./mcp-configuration.md). -## How It Works +## How it works -### File Generation +Both wrappers modify your Metro config in the same way under the hood: -The wrapper automatically generates `storybook.requires.ts` (or `.js`) containing: +**File generation** — Automatically generates `storybook.requires.ts` (or `.js`) containing story imports, addon registration, preview configuration, and hot module reloading support. -1. **Story imports** - Based on patterns in your `main.ts` -2. **Addon registration** - Loads configured addons -3. **Preview configuration** - Applies global decorators and parameters -4. **Hot module reloading** - Automatic updates when stories change +**Custom resolver** — Enables package exports for Storybook's ESM-based packages, handles platform-specific modules (like `uuid`), and filters out template files that can cause Metro to crash. -### Custom Resolver +**Composition** — Both wrappers are composable with other config wrappers. Chain them like `withStorybook(withNativeWind(config))`. The order may matter depending on the other wrappers. -The wrapper modifies Metro's resolver to: +## Production builds -- Enable package exports for Storybook packages since its a esm based package -- Handle platform-specific modules (like `uuid`) -- Filter out template files from story loading which can cause metro to crash +**Bundler-agnostic wrapper**: Storybook is automatically excluded when `STORYBOOK_ENABLED` is not set. Nothing to configure. -## Production Builds - -### Removing Storybook from Production - -For production builds, you can completely remove Storybook code by setting `enabled: false`: +**Metro-specific wrapper**: Set `enabled: false` to strip all Storybook code: ```js -// metro.config.js -const STORYBOOK_ENABLED = process.env.STORYBOOK_ENABLED === 'true'; - module.exports = withStorybook(config, { - enabled: STORYBOOK_ENABLED, + enabled: process.env.STORYBOOK_ENABLED === 'true', }); ``` -When storybook is disabled the withStorybook wrapper will: - -- Replace all `@storybook/*` and `storybook/*` imports with empty modules -- Stub out your Storybook config directory imports - -Note that if you try to render Storybook when it is disabled you will get a blank screen with a warning message. - -If you want to conditionally swap between your app and Storybook you can use the following pattern: - -```tsx -// App.tsx -import { AppRegistry } from 'react-native'; - -let AppEntryPoint = App; - -if (process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true') { - AppEntryPoint = require('./.rnstorybook').default; -} - -export default AppEntryPoint; -``` - -Or alternatively put storybook in its own screen that you can only access when Storybook is enabled. - ## Troubleshooting -### Common Issues - -1. **Stories not found** - - Verify `configPath` points to correct directory - - Check story patterns in `main.ts` - - Ensure Metro cache is cleared: `npx react-native start --reset-cache` +**Stories not found** — Verify `configPath` points to the correct directory, check story patterns in `main.ts`, and clear the Metro cache: `npx react-native start --reset-cache` or `npx expo start --clear`. -2. **WebSocket connection failed** - - Check if port is already in use - - Verify `host` matches your development setup - - verify that host and port match in both the `withStorybook` configuration and the `getStorybookUI` call in your app code - - For physical devices, use machine's IP instead of `localhost` +**WebSocket connection failed** — Check if the port is already in use, verify the host matches your setup, and use your machine's IP instead of `localhost` for physical devices. With the bundler-agnostic wrapper, WebSocket settings are auto-injected; with the Metro-specific wrapper, ensure host/port match between `withStorybook` options and your `getStorybookUI` call. -3. **Production bundle includes Storybook** - - Set `enabled: false` in your metro config - - Conditionally import Storybook UI in your app code based on environment variables +**Production bundle includes Storybook** — With the bundler-agnostic wrapper, ensure you're not setting `STORYBOOK_ENABLED=true` in production builds. With the Metro-specific wrapper, set `enabled: false`. diff --git a/docs/docs/intro/configuration/storybook-ui-configuration.md b/docs/docs/intro/configuration/storybook-ui-configuration.md index 12d86f244b..a5bc583cc7 100644 --- a/docs/docs/intro/configuration/storybook-ui-configuration.md +++ b/docs/docs/intro/configuration/storybook-ui-configuration.md @@ -8,19 +8,17 @@ The `getStorybookUI` function configures how Storybook renders and behaves in yo ## Basic Usage -```typescript +```ts import { view } from './storybook.requires'; const StorybookUIRoot = view.getStorybookUI({ // Options go here }); - -export default StorybookUIRoot; ``` ## Complete Options Reference -```typescript +```ts const StorybookUIRoot = view.getStorybookUI({ // UI Behavior onDeviceUI: true, @@ -62,7 +60,7 @@ const StorybookUIRoot = view.getStorybookUI({ - **Purpose**: Enable or disable the on-device UI (story navigator, addons panel) - **Use Case**: Set to `false` when using only WebSocket control or custom UI -```typescript +```ts // Story-only mode (no UI controls) const StorybookUIRoot = view.getStorybookUI({ onDeviceUI: false, @@ -75,7 +73,7 @@ const StorybookUIRoot = view.getStorybookUI({ - **Purpose**: Remember the last viewed story between app launches - **Storage**: Requires a storage implementation -```typescript +```ts const StorybookUIRoot = view.getStorybookUI({ shouldPersistSelection: true, storage: { @@ -95,7 +93,7 @@ const StorybookUIRoot = view.getStorybookUI({ - Object: `{ kind: string, name: string }` - String: `'kind--name'` -```typescript +```ts // Object format (recommended) const StorybookUIRoot = view.getStorybookUI({ initialSelection: { @@ -120,7 +118,7 @@ const StorybookUIRoot = view.getStorybookUI({ #### Using Built-in Themes -```typescript +```ts import { theme, darkTheme } from '@storybook/react-native-theming'; // Use built-in light theme @@ -136,7 +134,7 @@ const StorybookUIRoot = view.getStorybookUI({ #### Custom Theme Structure -```typescript +```ts import { view } from './storybook.requires'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -244,8 +242,6 @@ const StorybookUIRoot = view.getStorybookUI({ }, }, }); - -export default StorybookUIRoot; ``` ### Storage Configuration @@ -255,7 +251,7 @@ export default StorybookUIRoot; - **Default**: No storage (selection not persisted) - **Purpose**: Persist UI state between sessions - **Interface**: - ```typescript + ```ts interface Storage { getItem: (key: string) => Promise; setItem: (key: string, value: string) => Promise; @@ -264,7 +260,7 @@ export default StorybookUIRoot; #### AsyncStorage (Most Common) -```typescript +```ts import { view } from './storybook.requires'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -274,15 +270,13 @@ const StorybookUIRoot = view.getStorybookUI({ setItem: AsyncStorage.setItem, }, }); - -export default StorybookUIRoot; ``` #### MMKV (High Performance) If you prefer using MMKV for better performance: -```typescript +```ts import { view } from './storybook.requires'; import { MMKV } from 'react-native-mmkv'; @@ -294,13 +288,17 @@ const StorybookUIRoot = view.getStorybookUI({ setItem: async (key, value) => storage.set(key, value), }, }); - -export default StorybookUIRoot; ``` ### WebSocket Options -Enable remote control of Storybook from external tools: +Enable remote control of Storybook from external tools. See [WebSocket Configuration](./websocket-configuration.md) for the full guide. + +:::tip Auto-injected by the bundler wrapper +When you configure `websockets: 'auto'` (or `{ host, port }`) in your `withStorybook` bundler wrapper, the WebSocket settings are automatically injected into the generated `storybook.requires` file. You do **not** need to set `enableWebsockets`, `host`, or `port` in `getStorybookUI` — they're already wired up. This applies to both the bundler-agnostic and Metro-specific wrappers. + +The options below are only needed if you're not using a `withStorybook` wrapper at all, or if you want to override the auto-injected values. +::: #### `enableWebsockets` (boolean) @@ -318,7 +316,8 @@ Enable remote control of Storybook from external tools: - **Default**: `7007` - **Purpose**: WebSocket server port -```typescript +```ts +// Only needed without a withStorybook wrapper, or to override auto-injected values const StorybookUIRoot = view.getStorybookUI({ enableWebsockets: true, host: '192.168.1.100', // Your machine's IP @@ -340,7 +339,7 @@ The `CustomUIComponent` option allows you to completely replace Storybook's defa Your custom UI component must implement the `SBUI` interface: -```typescript +```ts type SBUI = (props: { story?: StoryContext; storyHash: API_IndexHash; @@ -364,7 +363,7 @@ type SBUI = (props: { This example shows a basic custom UI with a modal-based story selector: -```typescript +```ts import AsyncStorage from '@react-native-async-storage/async-storage'; import { Button, @@ -439,8 +438,6 @@ const StorybookUIRoot = view.getStorybookUI({ setItem: AsyncStorage.setItem, }, }); - -export default StorybookUIRoot; ``` #### Alternative Approaches @@ -449,7 +446,7 @@ export default StorybookUIRoot; You can also use the CustomUIComponent property to pass the lite ui for a ui that requires less dependencies and is more compatible with other platforms. -```typescript +```ts import { view } from './storybook.requires'; import { LiteUI } from '@storybook/react-native-ui-lite'; @@ -457,23 +454,19 @@ const StorybookUIRoot = view.getStorybookUI({ CustomUIComponent: LiteUI, // Lightweight alternative to full UI // ... other options }); - -export default StorybookUIRoot; ``` ##### Conditional Custom UI You can conditionally use custom UI based on environment or user preferences: -```typescript +```ts import { view } from './storybook.requires'; const StorybookUIRoot = view.getStorybookUI({ CustomUIComponent: Platform.OS === 'windows' ? MyCustomUI : undefined, // ... other options }); - -export default StorybookUIRoot; ``` #### Important Notes @@ -489,7 +482,7 @@ export default StorybookUIRoot; ### Standard Setup -```typescript +```ts import { view } from './storybook.requires'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -499,26 +492,36 @@ const StorybookUIRoot = view.getStorybookUI({ setItem: AsyncStorage.setItem, }, }); - -export default StorybookUIRoot; ``` -### Testing Setup +### WebSocket-controlled Testing Setup + +This option is for testing tools such as Chromatic, enabling the tool to drive the UI via WebSockets. If you're using the `withStorybook` wrapper with `websockets: 'auto'`, you only need to disable the on-device UI since the WebSocket connection is already configured: -```typescript +```ts import { view } from './storybook.requires'; import AsyncStorage from '@react-native-async-storage/async-storage'; const StorybookUIRoot = view.getStorybookUI({ - onDeviceUI: false, // No UI for automated tests + onDeviceUI: false, // No UI — controlled via WebSocket + storage: { + getItem: AsyncStorage.getItem, + setItem: AsyncStorage.setItem, + }, +}); +``` + +Without a `withStorybook` wrapper, you'll need to set the WebSocket options manually: + +```ts +const StorybookUIRoot = view.getStorybookUI({ + onDeviceUI: false, enableWebsockets: true, - host: 'localhost', // use websocket server to control storybook + host: 'localhost', port: 7007, storage: { getItem: AsyncStorage.getItem, setItem: AsyncStorage.setItem, }, }); - -export default StorybookUIRoot; ``` diff --git a/docs/docs/intro/configuration/websocket-configuration.md b/docs/docs/intro/configuration/websocket-configuration.md index d068f6f56d..d0864a41ed 100644 --- a/docs/docs/intro/configuration/websocket-configuration.md +++ b/docs/docs/intro/configuration/websocket-configuration.md @@ -18,64 +18,45 @@ When WebSocket is enabled, Storybook creates a server that allows bidirectional ## Basic Setup -### Enable WebSocket in UI +### Recommended: Bundler-agnostic wrapper -```typescript -import { view } from './storybook.requires'; +If you're using the bundler-agnostic `withStorybook` (from `@storybook/react-native/withStorybook`), WebSocket configuration is handled automatically. Pass `websockets: 'auto'` in your config or set environment variables: -const StorybookUIRoot = view.getStorybookUI({ - enableWebsockets: true, - host: 'localhost', - port: 7007, -}); -``` - -### Enable WebSocket in Metro - -```js +```ts // metro.config.js -const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); +const { withStorybook } = require('@storybook/react-native/withStorybook'); module.exports = withStorybook(config, { - websockets: { - port: 7007, - host: 'localhost', - }, + websockets: 'auto', }); ``` -## Configuration Options +The wrapper auto-detects your LAN IP, starts the WebSocket server, and injects the connection settings into the generated `storybook.requires` file — no manual matching needed in `getStorybookUI`. -### UI Configuration (`getStorybookUI`) +You can also configure WebSockets via environment variables: -```typescript -const StorybookUIRoot = view.getStorybookUI({ - // Enable WebSocket server - enableWebsockets: true, +```sh +STORYBOOK_ENABLED=true STORYBOOK_WS_HOST=192.168.1.100 STORYBOOK_WS_PORT=7007 expo start +``` - // Server hostname - host: 'localhost', // or '192.168.1.100' for network access +See [Environment Variables](./environment-variables.md) for all supported variables. - // Server port - port: 7007, -}); -``` +### Metro-specific setup -### Metro Configuration (`withStorybook`) +If you're using the Metro-specific `withStorybook` (from `@storybook/react-native/metro/withStorybook`), you need to configure WebSockets in two places: -```js +**Metro config:** + +```ts module.exports = withStorybook(config, { websockets: { - // Server port (should match UI config) port: 7007, - - // Server hostname host: 'localhost', }, }); ``` -Or use auto host detection: +Or use auto host detection (`'auto'` support is available from v10.2): ```js module.exports = withStorybook(config, { @@ -83,11 +64,19 @@ module.exports = withStorybook(config, { }); ``` -`'auto'` support is available from v10.2. +**Storybook UI (`getStorybookUI`):** + +```ts +const StorybookUIRoot = view.getStorybookUI({ + enableWebsockets: true, + host: 'localhost', + port: 7007, +}); +``` To enable `https` and `wss`, pass TLS credentials: -```js +```ts const fs = require('fs'); module.exports = withStorybook(config, { @@ -103,9 +92,15 @@ module.exports = withStorybook(config, { ### iOS Physical Device -For iOS devices, use your machine's IP address: +For iOS devices, use your machine's IP address. With the bundler-agnostic wrapper, use `websockets: 'auto'` or set `STORYBOOK_WS_HOST`: -```typescript +```sh +STORYBOOK_ENABLED=true STORYBOOK_WS_HOST=192.168.1.100 expo start +``` + +With the Metro-specific wrapper, set the host in both config and UI: + +```ts const StorybookUIRoot = view.getStorybookUI({ enableWebsockets: true, host: '192.168.1.100', // Your machine's IP @@ -115,21 +110,17 @@ const StorybookUIRoot = view.getStorybookUI({ ### Android Emulator -For Android emulator, use the special IP address: +For Android emulator, use the special IP address `10.0.2.2`. With the bundler-agnostic wrapper, set it via env var: -```typescript -const StorybookUIRoot = view.getStorybookUI({ - enableWebsockets: true, - host: Platform.OS === 'android' ? '10.0.2.2' : 'localhost', - port: 7007, -}); +```sh +STORYBOOK_ENABLED=true STORYBOOK_WS_HOST=10.0.2.2 expo start ``` ## Remote Control API ### Connecting from External Client -```javascript +```ts // Node.js client example const WebSocket = require('ws'); @@ -155,8 +146,9 @@ You can find all the events in `storybook/internal/core-events` #### Select Story -```javascript -import {SET_CURRENT_STORY} from `storybook/internal/core-events` +```ts +import { SET_CURRENT_STORY } from 'storybook/internal/core-events'; + // Select a story by ID ws.send( JSON.stringify({ @@ -168,24 +160,22 @@ ws.send( ## Simple Screenshot Example -```javascript -const ws = new WebSocket("ws://localhost:7007"); +```ts +const ws = new WebSocket('ws://localhost:7007'); async function takeScreenshot(name: string) { - execSync( - `xcrun simctl io booted screenshot --type png screenshots/${name}.png`, - ); + execSync(`xcrun simctl io booted screenshot --type png screenshots/${name}.png`); } ws.onopen = () => { - console.log("connected"); + console.log('connected'); ws.send( JSON.stringify({ - type: "setCurrentStory", - args: [{ viewMode: "story", storyId: "button--basic" }], - }), + type: 'setCurrentStory', + args: [{ viewMode: 'story', storyId: 'button--basic' }], + }) ); - takeScreenshot("button-basic"); + takeScreenshot('button-basic'); }; ``` diff --git a/docs/docs/intro/development-workflows.md b/docs/docs/intro/development-workflows.md index a6c01bd1a5..caedf2ba7f 100644 --- a/docs/docs/intro/development-workflows.md +++ b/docs/docs/intro/development-workflows.md @@ -37,7 +37,7 @@ Begin with the `TextInput` component since it's used by multiple parts of the fo #### Create the Component Shell -```typescript +```ts // components/TextInput/TextInput.tsx import React from 'react'; import { TextInput as RNTextInput, View, Text, StyleSheet } from 'react-native'; @@ -91,7 +91,7 @@ const styles = StyleSheet.create({ #### Create the Story -```typescript +```ts // components/TextInput/TextInput.stories.tsx import type { Meta, StoryObj } from '@storybook/react-native'; import { TextInput } from './TextInput'; @@ -147,7 +147,7 @@ Now open Storybook to see your component in action: If you have a Storybook route in your app for example you could use deep linking -```bash +```sh npx uri-scheme open "myapp://storybook?STORYBOOK_STORY_ID=components-textinput--default" --ios ``` @@ -165,7 +165,7 @@ Use Storybook to iterate on your component: Use the Controls addon to experiment with different props without editing code: -```typescript +```ts // Enhanced story with controls export const Playground: Story = { args: { @@ -191,7 +191,7 @@ Update your component and see changes instantly thanks to hot reloading. Once `TextInput` is solid, move to the `Button` component: -```typescript +```ts // components/Button/Button.tsx import React from 'react'; import { TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native'; @@ -245,7 +245,7 @@ const styles = StyleSheet.create({ Create stories for all button states: -```typescript +```ts // components/Button/Button.stories.tsx export const Primary: Story = { args: { title: 'Sign In' }, @@ -266,7 +266,7 @@ export const Disabled: Story = { Test each state: -```bash +```sh # Jump directly to the loading state npx uri-scheme open "myapp://storybook?STORYBOOK_STORY_ID=components-button--loading" --ios ``` @@ -275,7 +275,7 @@ npx uri-scheme open "myapp://storybook?STORYBOOK_STORY_ID=components-button--loa Finally, build the complete login form using your tested components: -```typescript +```ts // components/LoginForm/LoginForm.tsx import React, { useState } from 'react'; import { View, StyleSheet } from 'react-native'; @@ -336,7 +336,7 @@ const styles = StyleSheet.create({ Create stories for the complete form: -```typescript +```ts // components/LoginForm/LoginForm.stories.tsx export const Default: Story = {}; @@ -380,7 +380,7 @@ Test your components on different screen sizes, and platforms (iOS/Android) runn Create stories for edge cases you discover: -```typescript +```ts export const LongLabel: Story = { args: { label: 'This is a very long label that might wrap to multiple lines and affect layout', diff --git a/docs/docs/intro/getting-started/expo-router.md b/docs/docs/intro/getting-started/expo-router.md index 63f7b962c6..d616766fa6 100644 --- a/docs/docs/intro/getting-started/expo-router.md +++ b/docs/docs/intro/getting-started/expo-router.md @@ -4,59 +4,69 @@ sidebar_position: 1 # Expo Router Setup -This guide covers setting up Storybook with Expo Router projects. Expo Router uses file-based routing, so the setup is slightly different from standard Expo projects. +This guide covers setting up Storybook as a route inside an Expo Router app. This approach renders Storybook within your app's navigation — useful if you want Storybook accessible alongside your app screens during development. -## Installation +:::tip Recommended: Entry-point swapping +For most projects, the simpler approach is **entry-point swapping** — the bundler swaps your entire app entry point for Storybook when `STORYBOOK_ENABLED=true` is set. No route setup needed, no Storybook code in production. See the [Getting Started guide](./index.md). -Use the Storybook CLI to add Storybook to your Expo Router project: - -```bash -npm create storybook@latest -``` - -When prompted, choose **recommended** and then **native**. +The Expo Router approach documented here is fully supported but not the preferred setup, because it embeds Storybook into your app's bundle and navigation. +::: ## Metro Configuration -Customize your Metro config to work with Storybook: +Generate a metro config if you don't have one: -```bash +```sh npx expo@latest customize metro.config.js ``` -Then update your `metro.config.js` file to include the Storybook wrapper: +Since this approach renders Storybook inside your app (not as a separate entry point), use the **Metro-specific** `withStorybook` wrapper with the `enabled` option: ```js +// metro.config.js const { getDefaultConfig } = require('expo/metro-config'); +// Use the Metro-specific wrapper for route-based setup const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); -module.exports = withStorybook(config); +module.exports = withStorybook(config, { + enabled: true, +}); ``` +:::warning Don't use the bundler-agnostic wrapper here +The bundler-agnostic `withStorybook` from `@storybook/react-native/withStorybook` performs entry-point swapping, which replaces your entire app with Storybook. For the Expo Router route approach, you need the Metro-specific wrapper from `@storybook/react-native/metro/withStorybook` so your app's routing stays intact. +::: + ## Creating the Storybook Route -Create a new route file for Storybook in your `app` directory: +Create a route file that sets up the Storybook UI directly. You'll import `view` from the generated `storybook.requires` file in your `.rnstorybook` folder: **app/storybook.tsx** ```tsx -export { default } from '../.rnstorybook'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { view } from '../.rnstorybook/storybook.requires'; + +const StorybookUIRoot = view.getStorybookUI({ + storage: { + getItem: AsyncStorage.getItem, + setItem: AsyncStorage.setItem, + }, +}); + +export default StorybookUIRoot; ``` ## Navigation Setup -Add navigation to your Storybook route. You can do this through: - -1. **Tab navigation** - Add a tab for Storybook in your tab navigator -2. **Stack navigation** - Add a button or link that navigates to `/storybook` -3. **Dev menu** - Create a development-only way to access Storybook +Add navigation to your Storybook route. You can do this through tab navigation, stack navigation, or a dev menu. ### Recommended Route Configuration -For the best Storybook experience, disable the header for the Storybook route. You can do this in your root layout file by configuring the screen options for the storybook route: +For the best Storybook experience, disable the header for the Storybook route in your root layout: **app/\_layout.tsx** @@ -100,9 +110,9 @@ This ensures that the Storybook route is only available during development and w ## Running Your App -Once set up, you can navigate to your Storybook route within your Expo Router app: +Once set up, start your app and navigate to the `/storybook` route: -```bash +```sh npm run start npm run ios # or npm run android ``` diff --git a/docs/docs/intro/getting-started/index.md b/docs/docs/intro/getting-started/index.md index f33d473e30..cfdaa98f8a 100644 --- a/docs/docs/intro/getting-started/index.md +++ b/docs/docs/intro/getting-started/index.md @@ -7,18 +7,20 @@ keywords: [react native, storybook, getting started, installation, setup, expo, # Getting started There are a few different ways to get started, the main recommendation is to use the CLI init. -This guide is intended for v10 of storybook. For v9 docs see the [v9.1 docs](https://github.com/storybookjs/react-native/tree/v9.1.4). +This guide is intended for Storybook version 10+. For v9 docs see the [v9.1 docs](https://github.com/storybookjs/react-native/tree/v9.1.4). React Native Storybook works with both plain React Native and Expo but examples are using Expo for brevity since Expo is officially recommended by Meta. For plain React Native projects there should be minimal differences. :::info Expo Router Users -If you're using **Expo Router** for file-based navigation, follow our dedicated [Expo Router Setup guide](./expo-router.md) instead of the standard setup below. Expo Router benefits from a specific configuration for routing integration. +For most projects, the simplest approach is entry-point swapping: the bundler swaps your app’s entire entry point for Storybook when `STORYBOOK_ENABLED=true` is set. No route setup needed, and no Storybook code ships to production. + +However, if you’re using **Expo Router** and want to render Storybook within your app’s navigation (instead of as a separate entry point), follow our dedicated [Expo Router Setup guide](./expo-router.md). This approach is fully supported, but not recommended, because it embeds Storybook into your app’s bundle and navigation. ::: :::tip AI-Assisted Setup If you're using an AI coding agent (Claude Code, Cursor, Windsurf, etc.), you can install our agent skills to get guided setup assistance: -```bash +```sh npx skills add storybookjs/react-native ``` @@ -34,92 +36,85 @@ For most existing projects we recommend adding Storybook via the CLI. Use the storybook cli to add Storybook to your project -```bash +```sh npm create storybook@latest ``` -### Update Metro Config +### Run Storybook -You should then wrap your metro config with the `withStorybook` function that will return an updated config object with the necessary options for Storybook. +The CLI sets everything up for you — it wraps your bundler config with `withStorybook`, generates the Storybook entry point, and adds convenience scripts to your `package.json`. No changes to `App.tsx` are needed. -If you have other config wrapper functions like `withNativeWind` you will want to chain these functions like `withStorybook(withNativeWind(config))` since they are composable (the order may be important). +When you set `STORYBOOK_ENABLED=true`, the wrapper automatically swaps your app's entry point with Storybook's entry point. When the variable is not set, your app runs normally with zero Storybook code in the bundle. -```js -// metro.config.js -const { getDefaultConfig } = require('expo/metro-config'); -const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); // <-- add this +The CLI adds these scripts to your `package.json`: + +```json +{ + "scripts": { + "storybook": "STORYBOOK_ENABLED=true expo start", + "storybook:ios": "STORYBOOK_ENABLED=true expo start --ios", + "storybook:android": "STORYBOOK_ENABLED=true expo start --android" + } +} +``` + +Then run: -/** @type {import('expo/metro-config').MetroConfig} */ -const config = getDefaultConfig(__dirname); +```sh +npm run storybook +``` + +:::note Windows +Use `cross-env` to set environment variables on Windows: -module.exports = withStorybook(config); // <-- add this +```json +"storybook": "cross-env STORYBOOK_ENABLED=true expo start" ``` -### Render storybook +::: -Now you need to update your app to render the Storybook component. One way to get storybook to render is to export the Storybook UI from the app entrypoint (usually `App.tsx`). Expand the section below for more information. +:::tip Bundler configuration +The CLI automatically wraps your `metro.config.js` with `withStorybook`. If you need to customize this — for example to chain it with other wrappers like `withNativeWind` — see [Metro Configuration](../configuration/metro-configuration.md) or [Manual Setup](./manual-setup.md). +::: -The Storybook component is in `.rnstorybook/index.tsx` +
+ Alternative: In-app integration (without entry-point swapping) -> Note the config folder changed to .rnstorybook starting in v9 +If you prefer to control how Storybook renders in your app (for example, to render it alongside your app or behind a toggle), you can import the Storybook UI directly in your `App.tsx`. This approach is fully supported: -```ts +```tsx // App.tsx import StorybookUI from './.rnstorybook'; export default StorybookUI; ``` -
- Rendering Storybook - -You don't have to replace your app entry point to render storybook this is just one way to get started. - -Other methods involve using env variables or application state to decide if Storybook should render. You can use any logic that you normally would in React to optionally render a component. - -Heres one example: +Or conditionally: ```tsx -function App() { - return ( - - Open up App.tsx to start working on your app! - - ); -} +import StorybookUI from './.rnstorybook'; +import { MyApp } from './MyApp'; -let AppEntryPoint = App; +const isStorybook = process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true'; -if (Constants.expoConfig?.extra?.storybookEnabled === 'true') { - AppEntryPoint = require('./.rnstorybook').default; +export default function App() { + return isStorybook ? : ; } - -export default AppEntryPoint; ``` -
- -### Run storybook +With this approach, use the Metro-specific `withStorybook` wrapper instead of the bundler-agnostic one, so you can control the `enabled` option directly: -You can then run your app using the normal commands for react native. +```js +// metro.config.js +const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); -```bash -# to run metro -npm run start -# to run launch ios -npm run ios -# to launch android -npm run android +module.exports = withStorybook(config, { + enabled: process.env.STORYBOOK_ENABLED === 'true', +}); ``` -Storybook will render where you have placed it. If you have used an env variable to enable Storybook you will want to make sure that is set when running metro. +See [Metro Configuration](../configuration/metro-configuration.md) for the full options reference. -If you're using an env variable you can setup some commands like this to run Storybook conditionally. - -```json -"storybook": "STORYBOOK_ENABLED='true' expo start", -"storybook:ios": "STORYBOOK_ENABLED='true' expo start --ios", -"storybook:android": "STORYBOOK_ENABLED='true' expo start --android" -``` +
## Project Template @@ -127,13 +122,13 @@ If you are starting a fresh project and you want to get setup with Storybook fro For Expo you can use this template with the following command: -```bash +```sh npx create-expo-app --template expo-template-storybook AwesomeStorybook ``` For React Native cli you can use this template -```bash +```sh npx @react-native-community/cli init MyApp --template react-native-template-storybook ``` @@ -152,3 +147,7 @@ Depending on your project setup and requirements, you may need different install - **[Expo Router Setup](./expo-router.md)** - For projects using Expo Router file-based navigation - **[Re.Pack Setup](./repack.md)** - For projects using Re.Pack (Rspack/Webpack) instead of Metro - **[Manual Setup](./manual-setup.md)** - For full control over the setup process or when the CLI doesn't work for your specific configuration + +## Migrating from an older setup? + +If you set up Storybook before entry-point swapping was available and want to switch from the deep app integration approach, see the [Migration Guide](./migrating-to-entry-point-swapping.md). diff --git a/docs/docs/intro/getting-started/manual-setup.md b/docs/docs/intro/getting-started/manual-setup.md index 72c60058c5..78d5be9978 100644 --- a/docs/docs/intro/getting-started/manual-setup.md +++ b/docs/docs/intro/getting-started/manual-setup.md @@ -2,53 +2,67 @@ sidebar_position: 2 --- -# Manual Setup +# Manual Setup (In-App Integration) -This guide covers setting up Storybook manually without using the CLI. This is useful if you want full control over the setup process or if the CLI doesn't work for your specific project configuration. +This guide covers integrating Storybook directly into your app — rendering it inside your `App.tsx`, behind a toggle, or on a dedicated screen. This gives you full control over when and how Storybook appears. -You can swap out npm for any other package manager. +:::tip Recommended: Automated setup with entry-point swapping +For most projects, the [Getting Started guide](./index.md) is the easier path. The CLI sets everything up automatically, and Storybook runs as its own entry point — no changes to your app code needed. + +The approach described on this page is fully supported but requires more manual work to set up and maintain. +::: + +## When to use this approach + +Use manual in-app integration when you want to: + +- Render Storybook behind a toggle or dev menu inside your app +- Control exactly where and how Storybook appears in your navigation +- Avoid entry-point swapping for architectural reasons + +If you're using **Expo Router** and want Storybook as a route (e.g. `/storybook`), see the dedicated [Expo Router Setup](./expo-router.md) guide instead. ## Dependencies -Install the required dependencies: +Install the required dependencies (swap `npm` for your preferred package manager): -```bash +```sh npm install storybook @storybook/react-native @react-native-async-storage/async-storage react-dom react-native-safe-area-context react-native-reanimated react-native-gesture-handler @gorhom/bottom-sheet react-native-svg ``` If you are working with dev clients or React Native CLI, make sure to install pods or run prebuild: -```bash +```sh cd ios; pod install; cd ..; ``` -## Files and Folders +## Storybook Configuration Create a folder called `.rnstorybook` with the required configuration files: -```bash +```sh mkdir .rnstorybook touch .rnstorybook/main.ts .rnstorybook/preview.tsx .rnstorybook/index.tsx ``` -### Main Configuration +### main.ts -In `main.ts`, configure the location of your stories: +Configure the location of your stories and on-device addons: ```ts import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], - addons: [], + deviceAddons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'], }; export default main; ``` -### Preview Configuration +### preview.tsx -In `preview.tsx`, set up any decorators or parameters: +Set up any global decorators or parameters: ```tsx import type { Preview } from '@storybook/react-native'; @@ -61,9 +75,9 @@ const preview: Preview = { export default preview; ``` -### Storybook UI Export +### index.tsx -In `index.tsx`, export the Storybook UI: +Export the Storybook UI. This is the component you'll render in your app: ```tsx import { view } from './storybook.requires'; @@ -81,52 +95,54 @@ export default StorybookUIRoot; ## Metro Configuration -Update `metro.config.js` to use our `withStorybook` wrapper function. +Since this approach renders Storybook inside your app (not as a separate entry point), use the **Metro-specific** `withStorybook` wrapper. This gives you direct control over the `enabled` option without triggering entry-point swapping. If you are using Expo and don't have a metro config, generate one first: -```bash +```sh npx expo customize metro.config.js ``` -Update `metro.config.js`: +**Expo:** ```js +// metro.config.js const { getDefaultConfig } = require('expo/metro-config'); const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); -const defaultConfig = getDefaultConfig(__dirname); +const config = getDefaultConfig(__dirname); -module.exports = withStorybook(defaultConfig); +module.exports = withStorybook(config, { + enabled: process.env.STORYBOOK_ENABLED === 'true', +}); ``` -For React Native CLI projects: +**React Native CLI:** ```js +// metro.config.js const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); const defaultConfig = getDefaultConfig(__dirname); - -/** - * Metro configuration - * https://reactnative.dev/docs/metro - * - * @type {import('metro-config').MetroConfig} - */ const config = {}; -// set your own config here 👆 - -const finalConfig = mergeConfig(defaultConfig, config); -module.exports = withStorybook(finalConfig); +module.exports = withStorybook(mergeConfig(defaultConfig, config), { + enabled: process.env.STORYBOOK_ENABLED === 'true', +}); ``` +:::warning Don't use the bundler-agnostic wrapper here +The bundler-agnostic `withStorybook` from `@storybook/react-native/withStorybook` performs entry-point swapping, which replaces your entire app entry point with Storybook. For in-app integration, you need the Metro-specific wrapper from `@storybook/react-native/metro/withStorybook` so your app remains the entry point and you control where Storybook renders. +::: + +See [Metro Configuration](../configuration/metro-configuration.md) for the full options reference. + ## storybook.requires.ts -You should also add a storybook-generate script to your project. +The `withStorybook` wrapper automatically generates and updates the `storybook.requires.ts` file whenever your app starts. You don't need to do anything extra. -In your `package.json` add the following script: +If you choose **not** to use the `withStorybook` wrapper at all, you'll need to generate/update this file manually by adding and triggering a script to your `package.json`: ```json { @@ -136,11 +152,13 @@ In your `package.json` add the following script: } ``` -You can use this for when you want to manually generate the `storybook.requires.ts` file. However the withStorybook function will automatically generate this file for you when you run your app. +Run `npm run storybook-generate` whenever you add, remove, or rename story files. + +## Rendering Storybook in Your App -## Rendering Storybook +Import `StorybookUIRoot` from `.rnstorybook` and render it somewhere in your app. -Update your app to render the Storybook component. One way to get Storybook to render is to export the Storybook UI from the app entry point (usually `App.tsx`): +**Always show Storybook** (simplest for development): ```tsx // App.tsx @@ -148,49 +166,38 @@ import StorybookUI from './.rnstorybook'; export default StorybookUI; ``` -### Conditional Rendering - -You don't have to replace your app entry point to render Storybook. You can use any logic that you normally would in React to optionally render a component. - -Here's an example using environment variables: +**Conditionally toggle** between your app and Storybook: ```tsx -function App() { - return ( - - Open up App.tsx to start working on your app! - - ); -} +// App.tsx +import StorybookUI from './.rnstorybook'; +import { MyApp } from './MyApp'; -let AppEntryPoint = App; +const isStorybook = process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true'; -if (Constants.expoConfig?.extra?.storybookEnabled === 'true') { - AppEntryPoint = require('./.rnstorybook').default; +export default function App() { + return isStorybook ? : ; } - -export default AppEntryPoint; ``` ## Running Storybook -You can then run your app as normal: +Start your app with the environment variable set: -```bash -npm run start -npm run ios # or npm run android +```sh +STORYBOOK_ENABLED=true expo start ``` -Storybook will render where you have placed it. If you have used an environment variable to enable Storybook, you will want to make sure that is set when running Metro. - -If you're using an environment variable, you can set up some commands like this to run Storybook conditionally: +Or add convenience scripts to your `package.json`: ```json { "scripts": { - "storybook": "STORYBOOK_ENABLED='true' expo start", - "storybook:ios": "STORYBOOK_ENABLED='true' expo start --ios", - "storybook:android": "STORYBOOK_ENABLED='true' expo start --android" + "storybook": "EXPO_PUBLIC_STORYBOOK_ENABLED=true expo start" } } ``` + +## Migrating to entry-point swapping + +If you later decide to switch to the recommended entry-point swapping approach, see the [Migration Guide](./migrating-to-entry-point-swapping.md) for step-by-step instructions. diff --git a/docs/docs/intro/getting-started/migrating-to-entry-point-swapping.md b/docs/docs/intro/getting-started/migrating-to-entry-point-swapping.md new file mode 100644 index 0000000000..66c13c355b --- /dev/null +++ b/docs/docs/intro/getting-started/migrating-to-entry-point-swapping.md @@ -0,0 +1,204 @@ +--- +sidebar_position: 4 +description: Migrate your existing Storybook React Native project from deep app integration to the new entry-point swapping setup. +keywords: [react native, storybook, migration, entry point swapping, withStorybook, upgrade] +--- + +# Migrating to Entry-Point Swapping + +Starting with v10.4, entry-point swapping is the default setup for new Storybook React Native projects. If your project uses the in-app integration approach, importing Storybook inside `App.tsx` and conditionally rendering it, that setup continues to work and is fully supported. However, entry-point swapping is simpler: the bundler swaps your app's entry point for Storybook's entry point automatically, so you don't need to touch your app code at all. + +This guide walks you through the migration. + +## What changes + +| Aspect | Old setup | New setup | +| ---------------------- | -------------------------------------------------------- | -------------------------------------------------------- | +| **Bundler wrapper** | `require('@storybook/react-native/metro/withStorybook')` | `require('@storybook/react-native/withStorybook')` | +| **Enabling Storybook** | `enabled` option in metro config | `STORYBOOK_ENABLED=true` environment variable | +| **App.tsx** | Conditional import/render of StorybookUI | No changes needed — entry point is swapped automatically | +| **On-device addons** | Listed in `addons` array in `main.ts` | Listed in `deviceAddons` array in `main.ts` | +| **WebSocket config** | Manually matched in metro config + `getStorybookUI` | Auto-injected via `withStorybook` or env vars | + +## Step 1: Update your bundler config + +Replace the metro-specific import with the bundler-agnostic wrapper: + +**Before:** + +```js +// metro.config.js +const { getDefaultConfig } = require('expo/metro-config'); +const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); + +const config = getDefaultConfig(__dirname); + +module.exports = withStorybook(config, { + enabled: process.env.STORYBOOK_ENABLED === 'true', + configPath: './.rnstorybook', + websockets: { port: 7007, host: 'localhost' }, +}); +``` + +**After:** + +```js +// metro.config.js +const { getDefaultConfig } = require('expo/metro-config'); +const { withStorybook } = require('@storybook/react-native/withStorybook'); + +const config = getDefaultConfig(__dirname); + +module.exports = withStorybook(config, { + configPath: './.rnstorybook', + websockets: { port: 7007, host: 'localhost' }, +}); +``` + +The new `withStorybook` reads configuration from environment variables, so you don't need to pass options. It also auto-detects whether you're using Metro or Re.Pack. + +## Step 2: Remove Storybook rendering from App.tsx + +You can now remove any conditional Storybook rendering from your app entry point. + +**Before:** + +```tsx +// App.tsx +import StorybookUI from './.rnstorybook'; +import { MyApp } from './MyApp'; + +const isStorybook = process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true'; + +export default function App() { + return isStorybook ? : ; +} +``` + +**After:** + +```tsx +// App.tsx +import { MyApp } from './MyApp'; + +export default function App() { + return ; +} +``` + +When `STORYBOOK_ENABLED=true` is set, the wrapper automatically swaps your app's entry point to `.rnstorybook/index`, so Storybook renders instead of your app. When it's not set, your app runs normally with zero Storybook code in the bundle. + +## Step 3: Ensure `.rnstorybook/index.tsx` is a valid entry point + +With entry-point swapping, `.rnstorybook/index.tsx` becomes your app's entry point when Storybook is enabled. It needs to register a root component — not just export one. + +If your `index.tsx` currently just exports a component (common in the in-app integration setup), update it to register itself: + +```tsx +// .rnstorybook/index.tsx +import { view } from './storybook.requires'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { AppRegistry } from 'react-native'; + +const StorybookUIRoot = view.getStorybookUI({ + storage: { + getItem: AsyncStorage.getItem, + setItem: AsyncStorage.setItem, + }, +}); + +AppRegistry.registerComponent('main', () => StorybookUIRoot); +``` + +## Step 4: Move on-device addons to `deviceAddons` + +In your `.rnstorybook/main.ts`, move any on-device addons from `addons` to the new `deviceAddons` property: + +**Before:** + +```ts +import type { StorybookConfig } from '@storybook/react-native'; + +const main: StorybookConfig = { + stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], + addons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'], +}; + +export default main; +``` + +**After:** + +```ts +import type { StorybookConfig } from '@storybook/react-native'; + +const main: StorybookConfig = { + stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], + deviceAddons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'], +}; + +export default main; +``` + +:::tip Automigration available +If you're using the Storybook CLI, the `rn-ondevice-addons-to-device-addons` automigration handles this step automatically. Any addon with "ondevice" in its name is moved to `deviceAddons`. + +```sh +npx storybook automigrate +``` + +::: + +**Why?** On-device addons contain React Native code that can't be evaluated on the server. When they're listed in `addons`, Storybook Core tries to load them as presets during operations like `extract`, which fails. The `deviceAddons` property ensures they're only loaded at runtime on the device. + +## Step 5: Update your scripts + +Update your `package.json` scripts to use the `STORYBOOK_ENABLED` environment variable: + +```json +{ + "scripts": { + "storybook": "STORYBOOK_ENABLED=true expo start", + "storybook:ios": "STORYBOOK_ENABLED=true expo start --ios", + "storybook:android": "STORYBOOK_ENABLED=true expo start --android" + } +} +``` + +For React Native CLI projects: + +```json +{ + "scripts": { + "storybook": "STORYBOOK_ENABLED=true react-native start", + "storybook:ios": "STORYBOOK_ENABLED=true react-native run-ios", + "storybook:android": "STORYBOOK_ENABLED=true react-native run-android" + } +} +``` + +:::note Windows users +On Windows, use `cross-env` to set environment variables: + +```json +{ + "scripts": { + "storybook": "cross-env STORYBOOK_ENABLED=true expo start" + } +} +``` + +::: + +After making all changes, restart Metro with a cache clear (`npx expo start --clear` or `npx react-native start --reset-cache`) and run your storybook script to verify everything works. + +:::tip WebSocket configuration +If you were manually configuring WebSocket host/port in both your metro config and `getStorybookUI`, you can now remove that duplication. The new `withStorybook` auto-injects WebSocket settings. See [Environment Variables](../configuration/environment-variables.md) for how to override them. +::: + +## Expo Router projects + +If you're using Expo Router with a dedicated `/storybook` route, you have two choices: + +1. **Keep the route approach** — your `/storybook` route still works. Move addons to `deviceAddons`, but **stay on the Metro-specific wrapper** (`@storybook/react-native/metro/withStorybook`) since the route approach needs your app to remain the entry point. See the [Expo Router Setup](./expo-router.md) guide. +2. **Switch to entry-point swapping** — remove the `/storybook` route, switch to the bundler-agnostic wrapper, and use `STORYBOOK_ENABLED=true` instead. This gives you a dedicated Storybook build with no app code in the bundle. diff --git a/docs/docs/intro/getting-started/repack.md b/docs/docs/intro/getting-started/repack.md index c3b02991bb..bdea32e82a 100644 --- a/docs/docs/intro/getting-started/repack.md +++ b/docs/docs/intro/getting-started/repack.md @@ -4,47 +4,41 @@ sidebar_position: 3 # Re.Pack Setup -This guide covers setting up Storybook with [Re.Pack](https://re-pack.dev/) projects that use Rspack or Webpack instead of Metro as the bundler. +This guide covers what's different when using [Re.Pack](https://re-pack.dev/) (Rspack/Webpack) instead of Metro. The Storybook configuration (`.rnstorybook` folder, stories, addons) is identical regardless of bundler, only the bundler config itself changes. For a ready-to-go starter project, check out the [RepackStorybookStarter](https://github.com/dannyhw/RepackStorybookStarter) repository. -## Installation +## Entry-point swapping (recommended) -Use the Storybook CLI to initialize your project: - -```bash -npm create storybook -- --type react_native --yes -``` - -## Install Reanimated and Worklets - -Storybook's default UI requires `react-native-reanimated` and `react-native-worklets`: - -```bash -npm install react-native-reanimated react-native-worklets -``` - -Then ensure the worklets babel plugin is in `babel.config.js`. It **must** be the last plugin in the list: +The bundler-agnostic `withStorybook` wrapper auto-detects Re.Pack and handles everything — entry-point swapping, story generation, and WebSocket setup. Follow the standard [Getting Started guide](./index.md) for the full walkthrough; the only difference is your bundler config file: ```js -// babel.config.js -module.exports = { - presets: [ - // your existing preset, e.g.: - 'module:@react-native/babel-preset', - ], - plugins: [ - // ... any other plugins - 'react-native-worklets/plugin', // must be last - ], -}; +// rspack.config.mjs +import * as Repack from '@callstack/repack'; +import { withStorybook } from '@storybook/react-native/withStorybook'; + +export default withStorybook( + Repack.defineRspackConfig({ + // ... your existing config + resolve: { + ...Repack.getResolveOptions({ + enablePackageExports: true, // required for Storybook package resolution + }), + }, + plugins: [new Repack.RepackPlugin()], + }) +); ``` -## Configure Rspack/Webpack +Then run with `STORYBOOK_ENABLED=true` as described in the Getting Started guide. No changes to `App.tsx` needed. + +:::warning +`enablePackageExports: true` is required so Rspack can correctly resolve Storybook's package exports. Without it, imports from Storybook packages will fail. +::: -Instead of wrapping Metro with `withStorybook`, add the `StorybookPlugin` to your rspack/webpack config plugins array. +## In-app integration (StorybookPlugin) -Use an environment variable (`STORYBOOK_ENABLED`) to control both the plugin behavior and a build-time constant for your app code: +If you prefer to render Storybook inside your app rather than using entry-point swapping, use the `StorybookPlugin` directly. This is the Re.Pack equivalent of the Metro-specific `withStorybook` wrapper described in the [Manual Setup](./manual-setup.md) guide. ```js // rspack.config.mjs @@ -58,7 +52,7 @@ export default Repack.defineRspackConfig({ // ... your existing config resolve: { ...Repack.getResolveOptions({ - enablePackageExports: true, // required for storybook package resolution + enablePackageExports: true, }), }, plugins: [ @@ -68,24 +62,12 @@ export default Repack.defineRspackConfig({ }), new StorybookPlugin({ enabled: storybookEnabled, - websockets: 'auto', }), - // ... your other plugins ], }); ``` -:::warning Important -`enablePackageExports: true` is required so rspack can correctly resolve Storybook's package exports (e.g. `@storybook/react-native/preview`). Without it, imports from Storybook packages will fail. -::: - -:::info -Unlike the Metro setup, there is no need to configure `require.context` support — rspack handles it natively. -::: - -## Create Entrypoint - -Conditionally render Storybook based on the `STORYBOOK_ENABLED` build-time constant. Since `StorybookPlugin` replaces Storybook imports with empty modules when disabled, you can import Storybook at the top level safely: +Then conditionally render Storybook in your app using the build-time constant: ```tsx // App.tsx @@ -98,14 +80,13 @@ export default function App() { return ; } - // Your existing app code here return ( - // ... + // ... your existing app ); } ``` -The `declare const` tells TypeScript about the global that rspack's `DefinePlugin` injects. When `STORYBOOK_ENABLED` is `false`, rspack dead-code-eliminates the Storybook branch entirely. +The `declare const` tells TypeScript about the global that Rspack's `DefinePlugin` injects. When `STORYBOOK_ENABLED` is `false`, Rspack dead-code-eliminates the Storybook branch entirely. ## Add Scripts @@ -119,13 +100,10 @@ The `declare const` tells TypeScript about the global that rspack's `DefinePlugi } ``` -Replace `react-native` with `rock` if your project uses Rock CLI. +## Re.Pack notes -## Run - -```bash -npm run storybook -``` +- Unlike Metro, there is no need to configure `require.context` support — Rspack handles it natively. +- Replace `react-native` with `rock` in your scripts if your project uses Rock CLI. ## StorybookPlugin Options diff --git a/docs/docs/intro/sharing-storybook.md b/docs/docs/intro/sharing-storybook.md index 99e4583037..21f32c3ce7 100644 --- a/docs/docs/intro/sharing-storybook.md +++ b/docs/docs/intro/sharing-storybook.md @@ -18,13 +18,13 @@ The following guide will be written for expo because its simpler to get setup, b If you don't already have an app, lets create one to get started with. -```bash +```sh npx create-expo-app --template expo-template-storybook@next AwesomeStorybook ``` This will create a new expo app with storybook already setup. -```bash +```sh cd AwesomeStorybook ``` @@ -34,7 +34,7 @@ Next we'll want to configure eas and setup eas updates. if you don't already have it then install the eas cli -```bash +```sh npm install -g eas-cli ``` @@ -42,7 +42,7 @@ Then lets have it setup the project for us. If you run each of these commands you'll end up with a project setup for eas builds and updates. -```bash +```sh eas login eas init eas build:configure -p all @@ -126,7 +126,7 @@ function config({ config }: ConfigContext): Partial { export default config; ``` -```bash +```sh eas build -p ios --submit --profile storybook ``` @@ -189,7 +189,7 @@ function config({ config }: ConfigContext): Partial { Now to create an internal build run -```bash +```sh eas build -p android --profile storybook-internal ``` diff --git a/docs/docs/intro/testing.md b/docs/docs/intro/testing.md index acc7509d29..3050e1eddf 100644 --- a/docs/docs/intro/testing.md +++ b/docs/docs/intro/testing.md @@ -26,7 +26,7 @@ Portable stories allow you to reuse your Storybook stories in external testing e Install the required testing dependencies: -```bash +```sh npm install --save-dev @testing-library/react-native @testing-library/jest-native jest ``` @@ -51,7 +51,7 @@ module.exports = config; Create a `setup-jest.ts` file for test configuration: -```typescript +```ts // setup-jest.ts import 'react-native-gesture-handler/jestSetup'; ``` @@ -60,7 +60,7 @@ import 'react-native-gesture-handler/jestSetup'; The `composeStories` utility processes all stories from a CSF file and returns them as testable components: -```typescript +```ts // Button.test.tsx import { render, screen } from '@testing-library/react-native'; import { composeStories } from '@storybook/react'; @@ -93,7 +93,7 @@ test('renders disabled button correctly', () => { For testing individual stories, use `composeStory`: -```typescript +```ts // Button.test.tsx import { render, screen, fireEvent } from '@testing-library/react-native'; import { composeStory } from '@storybook/react'; @@ -116,7 +116,7 @@ test('button click handler is called', () => { For stories that use global decorators or parameters, set up project annotations: -```typescript +```ts // setup-portable-stories.ts import { setProjectAnnotations } from '@storybook/react'; import * as previewAnnotations from '../.rnstorybook/preview'; @@ -137,7 +137,7 @@ const config = { #### Testing Controls -```typescript +```ts // TextInput.test.tsx import { render, screen } from '@testing-library/react-native'; import { composeStories } from '@storybook/react'; @@ -195,7 +195,7 @@ I also recommend setting onDeviceUI to false in your Storybook config to avoid i Install Maestro CLI: -```bash +```sh # macOS brew tap mobile-dev-inc/tap brew install maestro @@ -249,7 +249,7 @@ Note I highly recommend using [Bun](https://bun.sh/) for running scripts, since Run the tests: -```bash +```sh # Start your Expo/React Native app with Storybook npm run start @@ -261,7 +261,7 @@ npm run test:maestro You can automatically generate Maestro test files from your stories: -```typescript +```ts // scripts/generate-maestro-tests.ts import { writeFileSync, mkdirSync } from 'fs'; import path from 'path'; @@ -327,7 +327,7 @@ run() Heres an example of how you can set up screenshot comparison to detect visual regressions: -```typescript +```ts // scripts/compare-screenshots.ts import * as fs from 'fs'; import * as path from 'path'; diff --git a/packages/ondevice-actions/README.md b/packages/ondevice-actions/README.md index b350111479..04ca69e5a7 100644 --- a/packages/ondevice-actions/README.md +++ b/packages/ondevice-actions/README.md @@ -16,7 +16,7 @@ Then, add following content to `.rnstorybook/main.ts`: import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { - addons: ['@storybook/addon-ondevice-actions'], + deviceAddons: ['@storybook/addon-ondevice-actions'], }; export default main; diff --git a/packages/ondevice-backgrounds/README.md b/packages/ondevice-backgrounds/README.md index 93ef57d46b..ac991afc16 100644 --- a/packages/ondevice-backgrounds/README.md +++ b/packages/ondevice-backgrounds/README.md @@ -16,7 +16,7 @@ Then, add following content to `.rnstorybook/main.ts`: import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { - addons: ['@storybook/addon-ondevice-backgrounds'], + deviceAddons: ['@storybook/addon-ondevice-backgrounds'], }; export default main; diff --git a/packages/ondevice-controls/README.md b/packages/ondevice-controls/README.md index 45ca992672..0a566fc26b 100644 --- a/packages/ondevice-controls/README.md +++ b/packages/ondevice-controls/README.md @@ -18,7 +18,7 @@ Then, add following content to `.rnstorybook/main.ts`: import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { - addons: ['@storybook/addon-ondevice-controls'], + deviceAddons: ['@storybook/addon-ondevice-controls'], }; export default main; diff --git a/packages/ondevice-notes/README.md b/packages/ondevice-notes/README.md index 1efeabb6c7..2ec950bff3 100644 --- a/packages/ondevice-notes/README.md +++ b/packages/ondevice-notes/README.md @@ -16,7 +16,7 @@ Then, add following content to `.rnstorybook/main.ts`: import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { - addons: ['@storybook/addon-ondevice-notes'], + deviceAddons: ['@storybook/addon-ondevice-notes'], }; export default main; diff --git a/packages/react-native/readme.md b/packages/react-native/readme.md index 39ec759c67..679c4685ad 100644 --- a/packages/react-native/readme.md +++ b/packages/react-native/readme.md @@ -163,13 +163,13 @@ For projects using [Re.Pack](https://re-pack.dev/) (Rspack/Webpack) instead of M ## Expo router specific setup -```bash +```sh npm create storybook@latest ``` choose recommended and then native -```bash +```sh npx expo@latest customize metro.config.js ``` @@ -226,7 +226,7 @@ import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], - addons: [], + deviceAddons: [], }; export default main; @@ -307,7 +307,7 @@ Currently, the addons available are: - [`@storybook/addon-ondevice-notes`](https://storybook.js.org/addons/@storybook/addon-ondevice-notes): Add some Markdown to your stories to help document their usage - [`@storybook/addon-ondevice-backgrounds`](https://storybook.js.org/addons/@storybook/addon-ondevice-backgrounds): change the background of storybook to compare the look of your component against different backgrounds -Install each one you want to use and add them to the `main.ts` addons list as follows: +Install each one you want to use and add them to the `deviceAddons` list in `main.ts` as follows: ```ts // .rnstorybook/main.ts @@ -315,7 +315,7 @@ import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { // ... rest of config - addons: [ + deviceAddons: [ '@storybook/addon-ondevice-notes', '@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-backgrounds', @@ -535,7 +535,7 @@ import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], - addons: ['@storybook/addon-ondevice-controls'], + deviceAddons: ['@storybook/addon-ondevice-controls'], features: { ondeviceBackgrounds: true, }, diff --git a/skills/setup-react-native-storybook/SKILL.md b/skills/setup-react-native-storybook/SKILL.md index 6029a61c2f..3ed04191c0 100644 --- a/skills/setup-react-native-storybook/SKILL.md +++ b/skills/setup-react-native-storybook/SKILL.md @@ -29,7 +29,7 @@ Four setup flows based on project type: ### 1. Run CLI Init -```bash +```sh npm create storybook -- --type react_native --yes # or: pnpm create storybook --type react_native --yes # or: bun create storybook --type react_native --yes @@ -59,7 +59,7 @@ export default StorybookUIRoot; If the project doesn't have `@react-native-async-storage/async-storage`, install it: -```bash +```sh npm install @react-native-async-storage/async-storage ``` @@ -88,7 +88,7 @@ How Storybook is rendered differs per flow - see the relevant reference file. ### 6. Run -```bash +```sh npm run start npm run ios # or npm run android ``` diff --git a/skills/setup-react-native-storybook/references/expo-router-setup.md b/skills/setup-react-native-storybook/references/expo-router-setup.md index 300d7e3002..1ec211eb14 100644 --- a/skills/setup-react-native-storybook/references/expo-router-setup.md +++ b/skills/setup-react-native-storybook/references/expo-router-setup.md @@ -4,7 +4,7 @@ For Expo projects using Expo Router file-based navigation. ## Step 1: Run CLI Init -```bash +```sh npm create storybook -- --type react_native --yes ``` @@ -12,7 +12,7 @@ npm create storybook -- --type react_native --yes Storybook's default UI depends on `react-native-reanimated` and `react-native-worklets`. If they're not already installed: -```bash +```sh npx expo install --fix react-native-reanimated react-native-worklets ``` @@ -22,7 +22,7 @@ Expo handles the babel plugin automatically. Generate metro config if needed: -```bash +```sh npx expo@latest customize metro.config.js ``` @@ -147,6 +147,6 @@ In both cases, `unstable_settings` ensures the app opens directly to Storybook w ## Step 7: Run -```bash +```sh npm run storybook ``` diff --git a/skills/setup-react-native-storybook/references/expo-setup.md b/skills/setup-react-native-storybook/references/expo-setup.md index a021d412f2..4e0d6f83b1 100644 --- a/skills/setup-react-native-storybook/references/expo-setup.md +++ b/skills/setup-react-native-storybook/references/expo-setup.md @@ -4,7 +4,7 @@ For Expo projects that do **not** use Expo Router. ## Step 1: Run CLI Init -```bash +```sh npm create storybook -- --type react_native --yes ``` @@ -12,7 +12,7 @@ npm create storybook -- --type react_native --yes Storybook's default UI depends on `react-native-reanimated` and `react-native-worklets`. If they're not already installed: -```bash +```sh npx expo install --fix react-native-reanimated react-native-worklets ``` @@ -22,7 +22,7 @@ Expo handles the babel plugin automatically. Generate metro config if needed: -```bash +```sh npx expo@latest customize metro.config.js ``` @@ -79,6 +79,6 @@ export default function App() { ## Step 6: Run -```bash +```sh npm run storybook ``` diff --git a/skills/setup-react-native-storybook/references/react-native-cli-setup.md b/skills/setup-react-native-storybook/references/react-native-cli-setup.md index 827ca59df4..a170c36ba9 100644 --- a/skills/setup-react-native-storybook/references/react-native-cli-setup.md +++ b/skills/setup-react-native-storybook/references/react-native-cli-setup.md @@ -4,7 +4,7 @@ For plain React Native projects using `@react-native-community/cli`. ## Step 1: Run CLI Init -```bash +```sh npm create storybook -- --type react_native --yes ``` @@ -12,7 +12,7 @@ npm create storybook -- --type react_native --yes Storybook's default UI depends on `react-native-reanimated` and `react-native-worklets`: -```bash +```sh npm install react-native-reanimated react-native-worklets ``` @@ -20,7 +20,7 @@ npm install react-native-reanimated react-native-worklets This allows using `process.env.STORYBOOK_ENABLED` in app code: -```bash +```sh npm install --save-dev babel-plugin-transform-inline-environment-variables ``` @@ -78,7 +78,7 @@ export default function App() { ## Step 6: Install Pods -```bash +```sh cd ios && pod install && cd .. ``` @@ -96,6 +96,6 @@ cd ios && pod install && cd .. ## Step 8: Run -```bash +```sh npm run storybook ``` diff --git a/skills/setup-react-native-storybook/references/repack-setup.md b/skills/setup-react-native-storybook/references/repack-setup.md index be94f446ce..bd667563a9 100644 --- a/skills/setup-react-native-storybook/references/repack-setup.md +++ b/skills/setup-react-native-storybook/references/repack-setup.md @@ -4,7 +4,7 @@ For React Native projects using [Re.Pack](https://re-pack.dev/) instead of Metro ## Step 1: Run CLI Init -```bash +```sh npm create storybook -- --type react_native --yes ``` @@ -12,7 +12,7 @@ npm create storybook -- --type react_native --yes Storybook's default UI requires both `react-native-reanimated` and `react-native-worklets`. Re.Pack projects often already have `react-native-reanimated` but **`react-native-worklets` must also be installed separately** — it is not bundled with reanimated: -```bash +```sh npm install react-native-reanimated react-native-worklets ``` @@ -113,7 +113,7 @@ Replace `rock` with `react-native` or your project's CLI if not using Rock. ## Step 6: Run -```bash +```sh npm run storybook ``` diff --git a/skills/writing-react-native-storybook-stories/SKILL.md b/skills/writing-react-native-storybook-stories/SKILL.md index c0fe78a204..00d208fcda 100644 --- a/skills/writing-react-native-storybook-stories/SKILL.md +++ b/skills/writing-react-native-storybook-stories/SKILL.md @@ -224,7 +224,7 @@ import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'], - addons: [ + deviceAddons: [ '@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-backgrounds', '@storybook/addon-ondevice-actions',