关于 writing-react-native-storybook-stories
这是一项使用 `@storybook/react-native` v10 和 CSF 为 React Native 组件编写 Story 的技能,涵盖故事结构、控件、装饰器与参数,也介绍在 Jest 测试中复用故事。
主要功能
- 01CSF 故事文件规范
- 02共享 args 与自定义渲染
- 03控件与 argTypes 配置
- 04参数与装饰器设置
- 05在 Jest 中复用故事
适用场景
- 01记录组件的不同变体
- 02配置交互式属性控件
- 03为故事添加 Provider
- 04在 Jest 测试中复用故事
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ storybookjs/ react-native --skill writing-react-native-storybook-stories安装后在 Claude Code 中输入/writing-react-native-storybook-stories确认已加载。
附带文件references/controls.md
详细介绍
React Native Storybook 故事
使用 @storybook/react-native v10 和组件故事格式(CSF)为 React Native 组件编写故事。
快速开始
最简故事文件:
import type { Meta, StoryObj } from '@storybook/react-native';
import { MyComponent } from './MyComponent';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Basic: Story = {
args: {
label: 'Hello',
},
};
文件约定
- 名称:
ComponentName.stories.tsx,与组件放在同一目录 - 从
@storybook/react-native导入Meta和StoryObj - 默认导出:
meta对象,并满足Meta<typeof Component>类型 - 命名导出:使用大驼峰命名的故事名称,类型为
StoryObj<typeof meta> - 使用
args指定属性,使用argTypes配置控件,使用parameters配置插件 - 使用
render定义自定义渲染函数,使用decorators添加包装器
故事模式
使用共享参数定义多个故事
export const Primary: Story = {
args: { variant: 'primary', title: 'Click me' },
};
export const Secondary: Story = {
args: { ...Primary.args, variant: 'secondary' },
};
自定义渲染函数
export const WithScrollView: Story = {
render: (args) => (
<ScrollView>
<MyComponent {...args} />
</ScrollView>
),
};
使用 Hook 进行渲染(必须是命名函数)
export const Interactive: Story = {
render: function InteractiveRender() {
const [count, setCount] = useReducer((s) => s + 1, 0);
return <Counter count={count} onPress={setCount} />;
},
};
操作(模拟回调)
import { fn } from 'storybook/test';
const meta = {
component: Button,
args: { onPress: fn() },
} satisfies Meta<typeof Button>;
也可以通过 argTypes 配置:
argTypes: { onPress: { action: 'pressed' } },
自定义故事名称
export const MyStory: Story = {
storyName: 'Custom Display Name',
args: { label: 'Hello' },
};
自定义标题 / 嵌套
const meta = {
title: 'NestingExample/Message/Bubble',
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
控件与 ArgTypes
有关控件类型的完整参考,请参阅 references/controls.md。
常见模式:
const meta = {
component: MyComponent,
argTypes: {
// Select dropdown
size: {
options: ['small', 'medium', 'large'],
control: { type: 'select' },
},
// Range slider
opacity: {
control: { type: 'range', min: 0, max: 1, step: 0.1 },
},
// Color picker
color: { control: { type: 'color' } },
// Conditional control (shows only when `advanced` arg is true)
padding: { control: 'number', if: { arg: 'advanced' } },
},
} satisfies Meta<typeof MyComponent>;
自动检测:TypeScript 属性类型会自动映射到控件(string → 文本,boolean → 布尔值,联合类型 → 选择框,number → 数字)。
参数
插件参数
parameters: {
// Markdown docs in the Notes addon tab
notes: `# MyComponent\nUsage: \`<MyComponent label="hi" />\``,
// Background options for Backgrounds addon
backgrounds: {
default: 'dark',
values: [
{ name: 'light', value: 'white' },
{ name: 'dark', value: '#333' },
],
},
},
RN 专属 UI 参数
| 参数 | 类型 | 描述 |
|---|---|---|
noSafeArea | boolean | 移除顶部安全区域内边距。使用此参数时,组件本身必须处理安全区域,因为 Storybook 不再提供安全区域内边距。建议使用 useSafeAreaInsets(),而不是 SafeAreaView:将内边距应用为容器的 paddingTop/paddingBottom;对于可滚动内容,请使用 contentContainerStyle 内边距,而不是用 SafeAreaView 包裹。 |
storybookUIVisibility | 'visible' | 'hidden' | UI 的初始可见性 |
hideFullScreenButton | boolean | 隐藏全屏切换按钮 |
layout | 'padded' | 'centered' | 'fullscreen' | 故事容器布局 |
参数可以在故事、meta(组件)或全局(preview.tsx)级别设置。
装饰器
在提供程序、布局或上下文中包装故事:
const meta = {
component: MyComponent,
decorators: [
(Story) => (
<View style={{ alignItems: 'center', justifyContent: 'center', flex: 1 }}>
<Story />
</View>
),
],
} satisfies Meta<typeof MyComponent>;
全局装饰器放在 .rnstorybook/preview.tsx 中:
import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
import type { Preview } from '@storybook/react-native';
const preview: Preview = {
decorators: [withBackgrounds],
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
backgrounds: {
default: 'plain',
values: [
{ name: 'plain', value: 'white' },
{ name: 'dark', value: '#333' },
],
},
},
};
export default preview;
配置
.rnstorybook/main.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-backgrounds',
'@storybook/addon-ondevice-actions',
'@storybook/addon-ondevice-notes',
],
framework: '@storybook/react-native',
};
export default main;
故事 glob 也支持多目录设置所需的对象形式:
stories: [
'../components/**/*.stories.?(ts|tsx|js|jsx)',
{ directory: '../other_components', files: '**/*.stories.?(ts|tsx|js|jsx)' },
],
可移植故事(测试)
在 Jest 测试中复用故事:
import { render, screen } from '@testing-library/react-native';
import { composeStories } from '@storybook/react';
import * as stories from './Button.stories';
const { Primary, Secondary } = composeStories(stories);
test('renders primary button', () => {
render(<Primary />);
expect(screen.getByText('Click me')).toBeTruthy();
});
// Override args in tests
test('renders with custom props', () => {
render(<Primary title="Custom" />);
expect(screen.getByText('Custom')).toBeTruthy();
});
对于单个故事,请使用 composeStory:
import { composeStory } from '@storybook/react';
import meta, { Primary } from './Button.stories';
const PrimaryStory = composeStory(Primary, meta);
在 Jest 设置文件中为测试设置全局注解:
// setup-portable-stories.ts
import { setProjectAnnotations } from '@storybook/react';
import * as previewAnnotations from '../.rnstorybook/preview';
setProjectAnnotations(previewAnnotations);
插件摘要
| 插件 | 包 | 用途 |
|---|---|---|
| Controls | @storybook/addon-ondevice-controls | 交互式编辑 props |
| Actions | @storybook/addon-ondevice-actions | 记录组件交互 |
| Backgrounds | @storybook/addon-ondevice-backgrounds | 更改故事背景 |
| Notes | @storybook/addon-ondevice-notes | 添加 Markdown 文档 |
评论
支持 Markdown