- Migration
In this version of storybook we've reworked the UI using some community react native packages. We've also overhauled the theme to match the web version.
Update all storybook dependencies to 8.3.5 or newer.
For example you may end up with something like this
// package.json
{
"devDependencies": {
"@storybook/react-native": "^8.3.5",
"@storybook/react": "^8.3.5",
"@storybook/addon-ondevice-controls": "^8.3.5",
"@storybook/addon-ondevice-actions": "^8.3.5",
"@storybook/addon-ondevice-backgrounds": "^8.3.5",
"@storybook/addon-ondevice-notes": "^8.3.5"
}
}
Add the new required dependencies to your project.
Expo:
npx expo install react-native-reanimated react-native-gesture-handler @gorhom/bottom-sheet react-native-svg
RN Cli:
npm install react-native-reanimated react-native-gesture-handler @gorhom/bottom-sheet react-native-svg
Make sure you use versions of those packages supported by your version of expo/react-native
Regenerate your .storybook/storybook.requires.ts
file by running yarn storybook-generate
.
You should see a new updateView
function in the file.
// metro.config.js
const path = require('path');
const withStorybook = require('@storybook/react-native/metro/withStorybook');
module.exports = withStorybook(config, {
// set to false to disable storybook specific settings
// you can use a env variable to toggle this
enabled: true,
// path to your storybook config folder
configPath: path.resolve(__dirname, './.storybook'),
});
For a full list of options, see the withStorybook
documentation.
In this version of storybook we've a lot of changes to the internals of react native storybook to make it more compatible with core storybook libraries. This means compatibility with the new v7 store and the api changes that comes with that.
Here are some of the other improvements:
- New storage option that lets you choose what storage solution you want to use (async storage/mmkv etc).
- Support for main.ts
- Dynamic imports enabled by the unstable_allowRequireContext option in metro config.
- you only need to generate your requires file when main.ts changes.
- Error boundaries for stories so your app shouldn't crash when a story throws an error.
- Improved markdown renderer for notes addon.
- Simpler setup for auto args.
Note
You should follow a different set of changes if you need to support storiesOf, see 6.5.x to 7.6.x with storiesOf support
Update all storybook dependencies to 7.6.10 or newer.
For example you may end up with something like this
{
"@storybook/react-native": "^7.6.10",
"@storybook/addon-ondevice-actions": "^7.6.10",
"@storybook/addon-ondevice-backgrounds": "^7.6.10",
"@storybook/addon-ondevice-controls": "^7.6.10",
"@storybook/addon-ondevice-notes": "^7.6.10"
}
Regenerate your storybook.requires
file by running yarn storybook-generate
.
It should now generate a storybook.requires.ts
file instead of a storybook.requires.js
file.
This provides the type for the new view export.
Update .storybook/index.js
to use the new getStorybookUI
function on the view
exported from storybook.requires.ts
.
You can also change this file to be called .storybook/index.tsx
.
You should also now pass a storage object to the getStorybookUI
function. This is used to persist the selected story between reloads.
// .storybook/index.tsx
import { view } from './storybook.requires';
import AsyncStorage from '@react-native-async-storage/async-storage';
const StorybookUIRoot = view.getStorybookUI({
storage: {
getItem: AsyncStorage.getItem,
setItem: AsyncStorage.setItem,
},
});
export default StorybookUIRoot;
Update your metro.config.js
to enable the unstable_allowRequireContext
option and you can now remove the sbmodern resolver if you have it.
Note
The unstable_allowRequireContext option requires at least react native 0.72
If you are using expo and you don't have a metro config file you can create one by running npx expo customize metro.config.js
.
You can also add here the generate function to automatically update the storybook.requires.ts
file when you start metro.
You only need to regenerate this file now when main.js updates since requireContext allows us to use dynamic imports.
// metro.config.js
const path = require('path');
const { getDefaultConfig } = require('expo/metro-config');
const { generate } = require('@storybook/react-native/scripts/generate');
generate({
configPath: path.resolve(__dirname, './.storybook'),
});
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
config.transformer.unstable_allowRequireContext = true;
config.resolver.sourceExts.push('mjs');
module.exports = config;
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const path = require('path');
const { generate } = require('@storybook/react-native/scripts/generate');
generate({
// update ./.storybook to your storybook folder
configPath: path.resolve(__dirname, './.storybook'),
});
const defaultConfig = getDefaultConfig(__dirname);
/**
* Metro configuration
* https://facebook.github.io/metro/docs/configuration
*
* @type {import('metro-config').MetroConfig}
*/
const config = {
transformer: {
unstable_allowRequireContext: true,
},
resolver: {
sourceExts: [...defaultConfig.resolver.sourceExts, 'mjs'],
},
};
module.exports = mergeConfig(defaultConfig, config);
You can now also remove anything from package.json scripts which would run generate before running storybook.
We've removed the types from @storybook/react-native
and now you should import them from @storybook/react
. This is to remove duplication and increase compatibility with core storybook libraries.
Heres an example story in version 7:
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta = {
component: Button,
argTypes: {
onPress: { action: 'pressed the button' },
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Basic: Story = {
args: {
text: 'Press me!',
},
};
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.
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-native';
const main: StorybookConfig = {
stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'],
addons: [
'@storybook/addon-ondevice-notes',
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-backgrounds',
'@storybook/addon-ondevice-actions',
],
};
export default main;
To update preview.js to preview.tsx you can use the Preview type from @storybook/react
import type { Preview } from '@storybook/react';
const preview: Preview = {
decorators: []
parameters: {}
};
export default preview;
If you are manually adding doctools to do auto args you can now remove this code since its automatically added now. To make it work you still need babel-plugin-react-docgen-typescript though.
-import { extractArgTypes } from "@storybook/react/dist/modern/client/docs/extractArgTypes";
-import { addArgTypesEnhancer, addParameters } from "@storybook/react-native";
-import { enhanceArgTypes } from "@storybook/core/docs-tools";
-addArgTypesEnhancer(enhanceArgTypes);
-addParameters({
- docs: {
- extractArgTypes,
- },
-});
In version 7 it wasn’t possible to update @storybook/react-native-server
as it was. Instead we’ve released a new ReactJS addon called @storybook/addon-react-native-server
. With this new addon you will need to setup a ReactJS storybook with a separate storybook config. The benefit of this setup is that its possible to get a web preview of the components if your components are compatible with react-native-web.
There will shortly be a proper guide to setting this up, however the basics will be covered here.
The setup is the same as @storybook/addon-react-native-web
but with the reactNativeServerOptions property in the config file.
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
With expo you should also add @expo/metro-runtime
.
"storybook:web": "storybook dev -p 6006 -c .storybook-web",
"build-storybook": "storybook build -c .storybook-web"
Add a main.ts
// .storybook-web/main.ts
import type { StorybookConfig } from '@storybook/react-webpack5';
type ServerStorybookConfig = StorybookConfig & {
reactNativeServerOptions: { host: string; port: number };
};
const main: ServerStorybookConfig = {
stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-react-native-web',
'@storybook/addon-react-native-server',
],
framework: {
name: '@storybook/react-webpack5',
options: {},
},
reactNativeServerOptions: {
// for android you should use your local ip address here
host: 'localhost',
port: 7007,
},
docs: {
autodocs: 'tag',
},
};
export default main;
Add a preview.tsx
// .storybook-web/preview.tsx
import type { Preview } from '@storybook/react';
const preview: Preview = {
decorators: [],
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;
In your .storybook/index.tsx
file where you call view.getStorybookUI
make sure you enable Web sockets and match the host and port config from your .storybook-web/main.ts
.
import { view } from './storybook.requires';
import AsyncStorage from '@react-native-async-storage/async-storage';
const StorybookUIRoot = view.getStorybookUI({
storage: {
getItem: AsyncStorage.getItem,
setItem: AsyncStorage.setItem,
},
enableWebsockets: true,
host: 'localhost',
port: 7007,
});
export default StorybookUIRoot;
Now just make sure you run storybook:web
before running your React Native Storybook and you should be able to sync up your stories between web and your mobile devices.
Storybook v7 doesn't support storiesOf in the same way as v6, but there is a compatibility mode that allows you to continue using storiesOf whilst you migrate your stories.
Warning
By opting into this compatibility mode you will lose out on some features of v7. We highly recommend moving to CSF stories.
StoriesOf will be removed in v8 along with knobs and testing on those deprecated features will be limited.
Update all storybook dependencies to 7.6.10 or newer.
For example you may end up with something like this
{
"@storybook/react-native": "^7.6.10",
"@storybook/addon-ondevice-actions": "^7.6.10",
"@storybook/addon-ondevice-backgrounds": "^7.6.10",
"@storybook/addon-ondevice-controls": "^7.6.10",
"@storybook/addon-ondevice-notes": "^7.6.10"
}
To opt in you can pass --v6-store to sb-rn-get-stories in the generate script.
{
"scripts": {
"storybook-generate": "sb-rn-get-stories --v6-store"
}
}
You should also now import storiesOf from @storybook/react-native/V6
this is necessary so that certain code paths don't run in v7 mode.
Now that you've updated the script you can regenerate your storybook.requires.js
file by running yarn storybook-generate
.
It should now have the updated @storybook/react-native/V6
import in it.
Update the import in .storybook/index.js
from @storybook/react-native
to @storybook/react-native/V6
. You can also change this file to be called .storybook/index.tsx
.
You should also make sure to add the storage prop to the getStorybookUI call. This lets you opt into using a different storage solution like mmkv or if you put async storage there it will continue to work as it did before.
// .storybook/index.tsx
import './storybook.requires';
import { getStorybookUI } from '@storybook/react-native/V6';
import AsyncStorage from '@react-native-async-storage/async-storage';
const StorybookUIRoot = getStorybookUI({
storage: {
getItem: AsyncStorage.getItem,
setItem: AsyncStorage.setItem,
},
});
export default StorybookUIRoot;
Where ever you use storiesOf you should now import it from @storybook/react-native/V6
.
import { storiesOf } from '@storybook/react-native/V6';
import { text } from '@storybook/addon-knobs';
import { withKnobs } from '@storybook/addon-ondevice-knobs';
import React from 'react';
import { Button } from './AnotherButton';
storiesOf('Another Button', module)
.addDecorator(withKnobs)
.add('another button example', () => <Button text={text('text', 'test2')} onPress={() => null} />)
.add('again', () => <Button text={text('text', 'text2')} onPress={() => null} />);
If you're using typescript you may notice that for v7 we've removed the types in @storybook/react-native
. Thats part of an effort to re-use more code, you should now import them from @storybook/react
instead.
To make storybook more compatible with core storybook libraries we are using some new dependencies. You will need to add these to your project.
yarn add -D @storybook/react-native @storybook/core/common @react-native-async-storage/async-storage react-native-safe-area-context react-dom
To use the controls addon you will need these dependencies
yarn add -D @storybook/addon-ondevice-controls @storybook/addons @storybook/addon-controls @react-native-community/datetimepicker @react-native-community/slider
To use the actions addon you will need these dependencies
yarn add -D @storybook/addon-ondevice-actions @storybook/addon-actions
Rename the storybook folder to .storybook
In 6.5 we use a script to generate your imports for stories and addons. This uses the new main.js
file to generate the storybook.requires.js
file. This file is then imported into the index.js
file.
Remove the configure call, story imports and addon imports and reduce the index file to have this content. The most important thing is to import the storybook.requires
file.
Also if your Storybook folder is called storybook
you should change it to .storybook
.
// .storybook/index.js
import { getStorybookUI } from '@storybook/react-native';
import './storybook.requires';
const StorybookUIRoot = getStorybookUI({
// options go here
});
export default StorybookUI;
In your .storybook
folder add the main.js
and preview.js
files.
In the stories field of your main.js
file update the regex to work for your project.
In the addons field list the addons you want to use, these must be compatible with React Native Storybook. Addons made for React Native are usually prefixed with addon-ondevice
.
// .storybook/main.js
module.exports = {
stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'],
addons: [
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-actions',
'@storybook/addon-ondevice-backgrounds',
],
};
Move any global decorators and parameters your have to preview.js
, if you don’t have any then just export an empty array for decorators and an empty object for parameters.
// .storybook/preview.js
import { View } from 'react-native';
export const decorators = [
// Using a decorator to apply padding for every story
(StoryFn) => (
<View style={{ flex: 1, padding: 8 }}>
<StoryFn />
</View>
),
];
export const parameters = {
my_param: 'anything',
};
In storybook 6.5 there are some new binaries that generate your story imports and one that watches for new files.
You can add these scripts to your package.json file.
{
"scripts": {
"storybook-generate": "sb-rn-get-stories",
"storybook-watch": "sb-rn-watcher"
}
}
You'll want to run the generate script whenever you add a new story file. Alternatively you can keep the watcher running.
There are some options you can pass to the both these scripts. You can see them by running yarn sb-rn-get-stories --help
and sb-rn-watcher --help
.
.description('Getter and watcher for react native storybook')
.option(
'-c, --config-path <path>',
'The path to your config folder relative to your project-dir',
'./.storybook'
)
.option('-a, --absolute', 'Use absolute paths for story imports');
We use the sbmodern resolver field in order to resolve the modern version of storybook packages. Doing this removes the polyfills that ship in commonjs modules and fixes multiple long standing issues such as the promises never resolving bug and more (caused by corejs promises polyfill).
Expo
First create metro config file if you don't have it yet.
npx expo customize metro.config.js
Then add sbmodern to the start of the resolver.resolverMainFields
list.
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
--module.exports = getDefaultConfig(__dirname);
++const defaultConfig = getDefaultConfig(__dirname);
++defaultConfig.resolver.resolverMainFields.unshift('sbmodern');
++module.exports = defaultConfig;
React native
module.exports = {
/* existing config */
resolver: {
resolverMainFields: ['sbmodern', 'react-native', 'browser', 'main'],
},
};
Whilst storiesOf will still work it is now deprecated so we recommend that you convert your stories to CSF.
There is a codemod for your convenience which should automatically make the code changes for you (make sure to update the glob to fit your files):
npx storybook@next migrate storiesof-to-csf --glob="src/**/*.stories.tsx"
Replace the storiesOf API with a default export that defines the title and component of your stories. Replace the add method with named exports that define each story. If you have any parameters or decorators, you can add them to the default export or to stories.
// Before
storiesOf('Button', module)
.addParameters({ myParam: "anything" })
.addDecorator((Story)=> <Wrapper>{Story}</Wrapper>)
.add('primary', () => <Button primary label="Button" />)
.add('secondary', () => <Button label="Button" />);
// After
export default {
title: 'Button',
component: Button,
// for all stories in this file
parameters: { myParam: "anything" },
};
export const Primary = { args: { primary: true, label: "button" } };
export const Secondary = {
// this gives the property "label" the default value "button"
args: { label: "button" }
// for just this story
decorators: [(Story) => (<Wrapper><Story/></Wrapper>)],
parameters: {myParam: "something else"}
};
The theme structure in Storybook 6.5 provides much more granular control over more of the Storybook UI, including addons, this unfortunately makes it difficult to provide backwards compatibility. If you were previously using a custom theme you will now need to migrate it to the new theme.
The themeable values are comprehensively listed in the Theme
type in
theme.ts.
Below the old theme keys are listed against a comparable key in the new theme, although bear in mind that there are many more aspects of the UI that can be themed now, this is just to help get you started:
backgroundColor
:backgroundColor
storyListBackgroundColor
:panel.backgroundColor
listItemTextColor
:storyList.storyTextColor
listItemActiveColor
:storyList.storySelectedBackgroundColor
listItemActiveTextColor
:storyList.storySelectedTextColor
headerTextColor
:storyList.headerTextColor
labelColor
:inputs.labelTextColor
borderColor
:panel.borderColor
,navigation.borderColor
,inputs.text.borderColor
,inputs.radio.borderColor
,inputs.swatch.borderColor
previewBorderColor
: The preview no longer has a border and uses an elevation shadow insteadbuttonTextColor
:tabs.inactiveTextColor
,button.primary.textColor
,button.secondary.textColor
buttonActiveTextColor
:tabs.activeTextColor
secondaryLabelColor
:inputs.slider.valueTextColor
The tabs were renamed to Sidebar, Canvas and Addons. So the test ids for the tabs are now:
BottomMenu.Sidebar, BottomMenu.Canvas, BottomMenu.Addons
And their text was updated to SIDEBAR, CANVAS, ADDONS.
The server is a package called @storybook/react-native-server
and its an optional companion app that lets you control the React Native Storybook UI thats on your device via a web UI.
In this version the configuration for that was changes slightly.
Now to configure the server you'll add a .storybook_server
folder with a main.js
. In this file you should put the same stories regex that you have in your .storybook/main.js
file.
module.exports = {
stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'],
env: () => ({}),
addons: ['@storybook/addon-essentials'],
};
Then in package.json
you'll need a new script
"start-server": "react-native-storybook-server"
For more information about this please read this blog post.