Skip to main content
Version 10 brings Storybook React Native in sync with Storybook core v10, introducing improved Metro configuration and a simplified API.

Migration Steps

1
Update Storybook dependencies to 10.x
2
You need to update all Storybook dependencies to version 10.x. This includes:
3
  • storybook package (core)
  • @storybook/react package
  • @storybook/react-native package
  • All @storybook/addon-ondevice-* packages
  • 4
    You can check the correct version by looking at the peerDependencies. Please refer to the core Storybook migration guide for more details on the breaking changes in Storybook core v10.
    5
    Example package.json after upgrade:
    6
    {
      "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"
      }
    }
    
    7
    Update your metro config
    8
    The withStorybook metro wrapper has been significantly simplified in v10. The key changes are:
    9
  • BREAKING: Named import required - withStorybook is now a named export instead of a default export
  • Import path unified - In v9, withStorybookConfig (from metro/withStorybookConfig) was a preview of the simplified API. In v10, this is now the standard behavior when importing withStorybook from metro/withStorybook
  • onDisabledRemoveStorybook is removed - When enabled: false, Storybook is automatically removed from the bundle (no separate flag needed)
  • Simpler defaults - Works out of the box with sensible defaults
  • 10
    The withStorybook function is now a named export. You must use const { withStorybook } = require(...) instead of a default import.
    11
    Before (v9):
    12
    If you were using the preview withStorybookConfig (recommended approach in v9):
    13
    const { withStorybookConfig } = require('@storybook/react-native/metro/withStorybookConfig');
    
    module.exports = withStorybookConfig(defaultConfig);
    
    14
    Or if you were using withStorybook with the old API (default export):
    15
    const withStorybook = require('@storybook/react-native/metro/withStorybook'); // ❌ Default export
    
    module.exports = withStorybook(defaultConfig, {
      enabled: process.env.STORYBOOK_ENABLED === 'true',
      onDisabledRemoveStorybook: true, // This option no longer exists in v10
      configPath: path.resolve(__dirname, './.rnstorybook'),
    });
    
    16
    After (v10):
    17
    const { withStorybook } = require('@storybook/react-native/metro/withStorybook'); // ✅ Named export
    
    // Basic usage - works out of the box with defaults
    module.exports = withStorybook(defaultConfig);
    
    // Or with options
    module.exports = withStorybook(defaultConfig, {
      // When false, automatically removes Storybook from bundle
      enabled: process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true',
      configPath: path.resolve(__dirname, './.rnstorybook'),
    });
    
    18
    Key improvements:
    19
  • BREAKING: Must use named import: const { withStorybook } = require(...) instead of default import
  • withStorybookConfig functionality is now the default behavior of withStorybook
  • When enabled: false, Storybook packages are automatically stubbed out (no need for onDisabledRemoveStorybook)
  • Default configPath is ./.rnstorybook
  • 20
    Simplify your App.tsx (or Expo Router routes)
    21
    In v10, you can now directly import and export Storybook without worrying about bundle size or conditional imports. The metro config automatically handles everything.
    22
    Before (v9):
    23
    You couldn’t safely import Storybook at the top level because it would be included in your production bundle, even when disabled. Then even if you used the onDisabledRemoveStorybook option you would still need to conditionally import Storybook in your app code to not cause a crash.
    24
    // App.tsx
    let AppEntryPoint = App;
    
    if (process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true') {
      // Conditional import to avoid bundling Storybook in production
      AppEntryPoint = require('./.rnstorybook').default;
    }
    
    export default AppEntryPoint;
    
    25
    After (v10):
    26
    Now you can safely import Storybook at the top level - metro will automatically remove it when enabled: false:
    27
    // App.tsx
    import StorybookUI from './.rnstorybook'; // ✅ Safe! Metro stubs this out when enabled: false
    import { Text, View } from 'react-native';
    
    const isStorybook = process.env.EXPO_PUBLIC_STORYBOOK_ENABLED === 'true';
    
    // this could be your app entrypoint
    const AppComponent = () => {
      return (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
          <Text>Hello World</Text>
        </View>
      );
    };
    
    export default function App() {
      return isStorybook ? <StorybookUI /> : <AppComponent />;
    }
    
    28
    The key difference: the top-level import is now safe! Metro automatically stubs out the .rnstorybook import when enabled: false, removing all Storybook code from your production bundle.
    29
    Or even simpler, with Expo Router, you can create a dedicated route and now not worry about conditional inline requires.
    30
    // app/storybook.tsx
    export { default } from '../.rnstorybook';
    
    31
    How it works:
    32
    The metro config (enabled flag) automatically handles bundle inclusion:
    33
  • When enabled: true - Storybook is included in the bundle
  • When enabled: false - The .rnstorybook import is automatically stubbed out as an empty module, and all Storybook dependencies are removed
  • 34
    Key benefits:
    35
  • Import Storybook anywhere without bundle size concerns
  • Works seamlessly with Expo Router - just create a /storybook route
  • No more worrying about top-level imports bloating your production bundle
  • Metro handles all the complexity automatically
  • 36
    Regenerate your requires file
    37
    After updating dependencies and configuration, regenerate your .rnstorybook/storybook.requires.ts file:
    38
    yarn storybook-generate
    
    39
    Or if you have the generate call in your metro config (recommended), just restart metro:
    40
    yarn start --reset-cache
    

    Summary of Breaking Changes

    Please review these breaking changes carefully before migrating.
    1. Metro config API changes:
      • BREAKING: withStorybook is now a named export - use const { withStorybook } = require(...) instead of default import
      • withStorybookConfig from metro/withStorybookConfig is removed
      • Use withStorybook from metro/withStorybook instead (the simplified API is now standard)
      • onDisabledRemoveStorybook option removed (automatic when enabled: false)
    2. Default config folder:
      • Confirmed as ./.rnstorybook (to avoid conflicts with web Storybook)
    3. Storybook core updated to v10:
    4. Simplified app entry:
      • Custom switcher components no longer needed for bundle optimization
      • Metro config handles conditional inclusion automatically

    Build docs developers (and LLMs) love