Development Workflows
This guide covers common development workflows and patterns when working with Storybook for React Native.
Project Setup
New Project
Quickly bootstrap a new project with Storybook pre-configured:
npx create-expo-app --template expo-template-storybook AwesomeStorybook
This template includes:
- Storybook configured in
.rnstorybook/
- Metro config with
withStorybook wrapper
- Example stories
- On-device addons (controls, actions, backgrounds)
npx @react-native-community/cli init MyApp --template react-native-template-storybook
The template provides:
- Complete Storybook setup
- Pre-configured Metro bundler
- Sample components with stories
Existing Project
Add Storybook to an existing React Native app:
Run the init command
npm create storybook@latest
This automatically installs dependencies and creates:
.rnstorybook/ configuration directory
main.ts for Storybook configuration
preview.tsx for global decorators/parameters
Update Metro configuration
Create or update metro.config.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.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true',
});
// metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const { withStorybook } = require('@storybook/react-native/metro/withStorybook');
const defaultConfig = getDefaultConfig(__dirname);
const config = {}; // Your custom config
const finalConfig = mergeConfig(defaultConfig, config);
module.exports = withStorybook(finalConfig, {
enabled: process.env.STORYBOOK_ENABLED === 'true',
});
Configure app entry point
Update your app entry (e.g., App.tsx):// Direct export (simplest)
export { default } from './.rnstorybook';
Or with conditional rendering:import StorybookUI from './.rnstorybook';
import { MyApp } from './MyApp';
const isStorybook = process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true';
export default function App() {
return isStorybook ? <StorybookUI /> : <MyApp />;
}
Install reanimated
Storybook UI requires react-native-reanimated:npx expo install react-native-reanimated
Add the plugin to babel.config.js:module.exports = {
plugins: ['react-native-reanimated/plugin'],
};
Writing Stories
Component Story Format (CSF)
Storybook uses CSF, an ES6 module-based standard:
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-native';
import { Button } from './Button';
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
text: 'Hello World',
color: 'purple',
},
};
export const Secondary: Story = {
args: {
...Primary.args,
color: 'blue',
},
};
File Organization
Organize stories alongside components:
components/
├─ Button/
│ ├─ Button.tsx
│ ├─ Button.stories.tsx
│ └─ Button.test.tsx
├─ TextInput/
│ ├─ TextInput.tsx
│ ├─ TextInput.stories.tsx
│ └─ TextInput.test.tsx
Story Configuration
Configure story paths in .rnstorybook/main.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',
'@storybook/addon-ondevice-backgrounds',
],
};
export default main;
Working with Addons
Available On-Device Addons
Controls
Actions
Backgrounds
Adjust component props in real-time:import type { Meta, StoryObj } from '@storybook/react-native';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
argTypes: {
color: {
control: 'color',
},
size: {
control: { type: 'select' },
options: ['small', 'medium', 'large'],
},
},
};
export default meta;
Log component interactions:export const WithActions: Story = {
args: {
onPress: action('pressed'),
onLongPress: action('long-pressed'),
},
};
Test components against different backgrounds:const meta: Meta<typeof Button> = {
component: Button,
parameters: {
backgrounds: {
values: [
{ name: 'red', value: '#f00' },
{ name: 'green', value: '#0f0' },
],
},
},
};
Decorators and Parameters
Component-Level Decorators
Wrap stories with common layout or context:
import { View } from 'react-native';
import type { Meta } from '@storybook/react-native';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
decorators: [
(Story) => (
<View style={{ alignItems: 'center', justifyContent: 'center', flex: 1 }}>
<Story />
</View>
),
],
};
export default meta;
Global Decorators
Apply decorators to all stories in .rnstorybook/preview.tsx:
import type { Preview } from '@storybook/react-native';
import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
import { View } from 'react-native';
const preview: Preview = {
decorators: [
withBackgrounds,
(Story) => (
<View style={{ flex: 1, padding: 16 }}>
<Story />
</View>
),
],
parameters: {
backgrounds: {
default: 'plain',
values: [
{ name: 'plain', value: 'white' },
{ name: 'dark', value: '#333' },
],
},
},
};
export default preview;
UI Configuration Parameters
Control the Storybook UI behavior per story:
export const FullScreen: Story = {
args: {
label: 'Button',
},
parameters: {
// Hide UI initially (fullscreen mode)
storybookUIVisibility: 'hidden',
// Remove safe area padding (edge-to-edge)
noSafeArea: true,
// Center the component
layout: 'centered',
// Hide fullscreen toggle button
hideFullScreenButton: true,
},
};
Available layout values: 'padded', 'centered', 'fullscreen'
Environment Switching
Toggle Between App and Storybook
Option 1: Direct Export
Option 2: Runtime Toggle
Option 3: Expo Router
Control via Metro config:// App.tsx
export { default } from './.rnstorybook';
// metro.config.js
module.exports = withStorybook(config, {
enabled: process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true',
});
When enabled: false, Metro removes Storybook from the bundle. Switch at runtime based on environment:// 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 ? <StorybookUI /> : <MyApp />;
}
Create a dedicated route:// app/storybook.tsx
export { default } from '../.rnstorybook';
Navigate to /storybook in your app.
Development Tips
Hot Reloading: Stories automatically reload when you save changes. Metro watches your story files and regenerates storybook.requires.ts.
Story Naming: Use descriptive names that become test descriptions when using portable stories in Jest.
Reset Metro Cache: If stories don’t appear, clear the cache:npx react-native start --reset-cache
Lite Mode (Reduced Bundle Size)
For apps that need smaller bundles or don’t want heavy dependencies:
// .rnstorybook/index.tsx
import { LiteUI } from '@storybook/react-native-ui-lite';
import { view } from './storybook.requires';
const StorybookUIRoot = view.getStorybookUI({
CustomUIComponent: LiteUI,
});
export default StorybookUIRoot;
// metro.config.js
module.exports = withStorybook(config, {
liteMode: true,
});
Lite mode removes dependencies like react-native-reanimated.
Feature Flags
Opt into new functionality via main.ts:
import type { StorybookConfig } from '@storybook/react-native';
const main: StorybookConfig = {
stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'],
addons: ['@storybook/addon-ondevice-controls'],
features: {
// New backgrounds API with full-screen support (v10.3+)
ondeviceBackgrounds: true,
},
};
export default main;
Feature flags become the default in the next major version. Enable them early to future-proof your setup.