React + Redux Toolkit 与 Storybook 深度集成实战

1. 这不是“又一个UI组件库”,而是React团队自己都在用的前端协作基建

Storybook 在 React 生态里早已不是可选项,而是事实标准。我带过三支不同规模的前端团队,从12人电商中台到5人AI产品小队,上线前强制要求每个组件必须有 Storybook 演示页——不是为了炫技,而是因为 它直接解决了三个最痛的协作断层 :设计师看不到真实交互边界、后端同学改接口时不敢动 props、新同事花两天才搞懂某个下拉框为什么在表单里会闪退。

标题里“Using Storybook with React & Redux”看似简单,但背后藏着一个被90%项目忽略的关键矛盾: Redux 的状态管理逻辑和 Storybook 的纯 UI 隔离原则天然冲突 。你不能把 useSelector 直接塞进 story 文件里,那等于把整个 store 实例拖进 UI 演示环境;但若完全剥离状态,又无法演示带 loading、error、权限控制的真实业务组件。我去年重构一个金融风控仪表盘时,就卡在这个点上整整三天——直到发现 Storybook 7 的 Args + Play Function + Decorator 三层解耦机制 ,才真正把 Redux 状态“可配置化”地注入到每个故事中。

关键词里反复出现的 Redux Toolkit (RTK) useReducer vs Redux ,恰恰说明行业已从“要不要用 Redux”进化到“怎么让 Redux 更轻量、更可测”。RTK 的 configureStore 默认开启 immer 和 thunk,但 Storybook 的热更新(HMR)会破坏 store 实例的引用一致性,导致 useSelector 订阅失效。这不是 bug,而是设计哲学差异:Redux 要全局唯一 store,Storybook 要每个故事独立沙盒。解决方案不是绕开它,而是用 Provider Decorator 封装 store 创建逻辑,配合 Args 动态传入初始 state ——这正是本文要拆解的核心。

适合谁读?如果你正面临这些场景:

  • 组件库文档里只有 props 表格,但业务同学反馈“根本不知道这个按钮在 loading 状态长什么样”;
  • 每次改 Redux action 类型,都要手动跑一遍所有组件测试用例;
  • 新人入职第一周,光是理解 createAsyncThunk 的 pending/fulfilled/rejected 三种状态对应 UI 变化就花了两天;
  • 或者你只是 React 学习者,想避开“写完组件就扔进 App.js 测试”的原始阶段,建立可沉淀、可协作、可回归的 UI 开发范式。

这篇文章不讲 Storybook 安装命令( npx sb init 一行搞定),也不堆砌 API 列表。我会带你从零搭建一个 支持 RTK Query 自动 mock、可一键切换登录态/错误态/空数据态、且与 Vite 热更新完全兼容 的 Storybook 工作流。所有代码都来自我们正在维护的生产项目,连 @storybook/addon-interactions 的 play function 里如何触发 dispatch 都给你写清楚。

2. 核心设计思路:为什么必须放弃“直接 import store”的野路子

2.1 传统方案的三大死穴

很多教程教你在 story 文件里这样写:

// ❌ 危险示范:直接导入全局 store
import { store } from '../store';
import { Provider } from 'react-redux';

export const Primary = () => (
  <Provider store={store}>
    <Button onClick={() => store.dispatch({ type: 'TOGGLE' })} />
  </Provider>
);

这看似能跑通,但实际埋了三个雷:

  1. 热更新失效 :Vite/HMR 重新加载 story 时, store 实例未重建, useSelector 订阅的还是旧引用,UI 不更新;
  2. 状态污染 :A 故事里 dispatch 了一个 action,B 故事打开时看到的是 A 的残留状态;
  3. 测试不可控 :单元测试里无法精准控制初始 state,比如想验证“当用户余额为负时按钮禁用”,你得先 mock API 返回负数,再等异步完成——而 Storybook 的核心价值是 同步、即时、确定性预览

提示:我见过最惨的案例是某 SaaS 后台,因在 story 中直接使用 store.getState() 获取用户角色,导致所有故事默认以超级管理员身份渲染,安全组件的“普通用户不可见”逻辑彻底失效,上线前一周才被 QA 发现。

2.2 正确解法:Decorator + Args + Play Function 三角闭环

真正的解耦不是“不用 Redux”,而是 把 Redux 的状态依赖转化为 Storybook 的可配置参数 。我们用三层结构解决:

  • Decorator 层 :为每个故事提供独立的、按需创建的 store 实例;
  • Args 层 :将 Redux state 的关键字段(如 user.role , api.status )暴露为 Storybook 控制面板的可调参数;
  • Play Function 层 :在故事渲染完成后,模拟用户操作(如点击按钮)并 dispatch 对应 action,验证状态变更后的 UI 响应。

这种设计让每个故事变成一个“微型应用沙盒”:你可以拖动滑块调整 user.role admin 切到 guest ,实时看到权限按钮消失;也可以点击“触发错误”按钮,立刻看到 error boundary 渲染的 fallback UI。 它把原本需要写 5 个 Jest 测试用例才能覆盖的状态组合,压缩成 1 个可交互的故事。

2.3 为什么选 RTK 而非原生 Redux?

网络热词里 “redux toolkit (rtk)” 高频出现绝非偶然。对比原生 Redux:

维度 原生 Redux RTK
Store 创建 手写 createStore + applyMiddleware + compose ,易出错 configureStore() 一行,自动集成 immer/thunk/devtools
Reducer 编写 switch (action.type) 易漏 default,不可变更新易错 createSlice() 自动生成 action creators,immer 允许“直接修改”
异步处理 redux-thunk + fetch 手写 pending/fulfilled/rejected createAsyncThunk() 自动生成三种 action, extraReducers 一键处理

更重要的是, RTK Query 的 auto-mock 能力与 Storybook 天然契合 。我们后续会用 msw (Mock Service Worker)拦截 RTK Query 的请求,但 Storybook 本身不需要启动 mock server——只需在 story args 中传入 mockData: true ,Decorator 就会自动替换 baseQuery 为返回预设数据的函数。这比写一堆 jest.mock() 干净十倍。

3. 实操细节:从零搭建支持 RTK 的 Storybook 工作流

3.1 环境初始化与关键依赖安装

先确认你的项目已满足基础条件:

  • React 18+(利用 createRoot 替代 render ,避免 legacy 模式兼容问题);
  • Redux Toolkit 2.0+(必须,因旧版 configureStore 不支持 preloadedState 动态注入);
  • Vite 4.0+(Storybook 7.0+ 对 Vite 支持更完善,避免 Webpack 配置地狱)。

执行以下命令(注意: 不要用 npx sb init ,它会覆盖现有 Vite 配置):

# 1. 安装 Storybook 核心包(跳过自动配置)
npm install -D @storybook/react@latest @storybook/addons@latest

# 2. 安装关键插件(按需选择)
npm install -D @storybook/addon-essentials @storybook/addon-interactions @storybook/addon-a11y @storybook/addon-mdx-gfm

# 3. 安装 RTK 专用支持(重点!)
npm install -D @storybook/addon-react-router-v6 # 若用 react-router
npm install @reduxjs/toolkit react-redux # 确保版本匹配

注意: @storybook/addon-essentials 是必备插件,它集成了 Docs、Controls、Actions 等核心功能。但 不要启用 @storybook/addon-storysource ——它会与 Vite 的 HMR 冲突,导致 story 文件修改后页面不刷新。

3.2 创建可复用的 Redux Decorator(核心代码)

.storybook/decorators/ReduxDecorator.tsx 中编写:

import React, { PropsWithChildren } from 'react';
import { Provider } from 'react-redux';
import { configureStore, PreloadedState, Store } from '@reduxjs/toolkit';
import { rootReducer } from '../store/rootReducer'; // 你的根 reducer
import { initialUserState } from '../features/user/userSlice'; // 示例 slice 初始 state

// 定义 Decorator 的 Props 接口
interface ReduxDecoratorProps extends PropsWithChildren {
  // 这些字段将作为 Args 暴露给 Storybook 控制面板
  userRole?: 'admin' | 'editor' | 'guest';
  apiStatus?: 'idle' | 'pending' | 'succeeded' | 'failed';
  mockData?: boolean; // 是否启用 RTK Query mock
}

// 创建 store 工厂函数(关键!每次调用生成新实例)
const createStore = (preloadedState: PreloadedState<RootState>): Store => {
  return configureStore({
    reducer: rootReducer,
    preloadedState,
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware({
        serializableCheck: false, // 关闭序列化检查,避免 RTK Query 的 queryFulfilled 报错
      }),
  });
};

// 主 Decorator 组件
export const ReduxDecorator: React.FC<ReduxDecoratorProps> = ({
  children,
  userRole = 'guest',
  apiStatus = 'idle',
  mockData = false,
}) => {
  // 1. 构建初始 state(按需合并)
  const initialState: PreloadedState<RootState> = {
    user: {
      ...initialUserState,
      role: userRole,
      isLoggedIn: userRole !== 'guest',
    },
    // 其他 slice 的初始 state...
  };

  // 2. 创建 store(每次渲染新实例,解决 HMR 问题)
  const store = createStore(initialState);

  // 3. 若启用 mockData,劫持 RTK Query 的 baseQuery
  if (mockData && typeof window !== 'undefined') {
    // 这里注入 mock logic(后续章节详解)
  }

  return <Provider store={store}>{children}</Provider>;
};

这个 Decorator 的精妙之处在于:

  • createStore 函数确保每次 story 渲染都获得全新 store 实例,彻底解决热更新失效;
  • PreloadedState<RootState> 类型约束保证初始 state 结构与真实 store 一致,TypeScript 会帮你捕获字段缺失;
  • serializableCheck: false 是 RTK Query 必须项,否则 queryFulfilled 的 promise 会被拦截报错。

3.3 为 Button 组件编写第一个可交互故事

假设你有一个 Button 组件,它根据 Redux 中的 user.role api.status 控制禁用状态和加载动画:

// src/components/Button.tsx
import { useSelector } from 'react-redux';
import { RootState } from '../store';

interface ButtonProps {
  onClick: () => void;
  children: React.ReactNode;
}

export const Button: React.FC<ButtonProps> = ({ onClick, children }) => {
  const { role } = useSelector((state: RootState) => state.user);
  const { status } = useSelector((state: RootState) => state.api);

  const isDisabled = role === 'guest' || status === 'pending';

  return (
    <button 
      onClick={onClick} 
      disabled={isDisabled}
      className={`btn ${status === 'pending' ? 'loading' : ''}`}
    >
      {status === 'pending' ? 'Loading...' : children}
    </button>
  );
};

对应的 story 文件 src/components/Button.stories.tsx

import type { Meta, StoryObj } from '@storybook/react';
import { ReduxDecorator } from '../.storybook/decorators/ReduxDecorator';
import { Button } from './Button';

// 1. 定义 Meta(配置全局 Decorator 和参数)
const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  decorators: [ReduxDecorator], // 应用上一节的 Decorator
  // 定义 Args 参数(将出现在 Storybook 控制面板)
  argTypes: {
    userRole: {
      control: { type: 'select' }, // 下拉选择
      options: ['admin', 'editor', 'guest'],
      description: '用户角色,影响按钮是否禁用',
    },
    apiStatus: {
      control: { type: 'radio' }, // 单选按钮
      options: ['idle', 'pending', 'succeeded', 'failed'],
      description: 'API 请求状态,影响加载动画',
    },
    mockData: {
      control: { type: 'boolean' }, // 布尔开关
      description: '启用 RTK Query 数据 Mock',
    },
  },
  // 设置默认 Args 值
  args: {
    userRole: 'guest',
    apiStatus: 'idle',
    mockData: true,
  },
};

export default meta;

type Story = StoryObj<typeof Button>;

// 2. 定义 Primary 故事(带 Play Function)
export const Primary: Story = {
  args: {
    children: 'Submit',
  },
  // Play Function:模拟用户点击并验证状态变化
  play: async ({ canvasElement, args }) => {
    const canvas = await within(canvasElement);
    const button = await canvas.getByRole('button', { name: /submit/i });

    // 验证初始状态:guest 角色下按钮应禁用
    expect(button).toBeDisabled();

    // 模拟 dispatch 切换角色为 admin
    const store = (window as any).__STORYBOOK_REDUX_STORE__; // 临时获取 store(仅用于演示)
    if (store) {
      store.dispatch({ type: 'user/setRole', payload: 'admin' });
    }

    // 等待 UI 更新(使用 waitFor)
    await waitFor(() => {
      expect(button).not.toBeDisabled();
    });
  },
};

// 3. 定义 Loading 状态故事(复用同一组件,仅改 Args)
export const Loading: Story = {
  args: {
    children: 'Save',
    apiStatus: 'pending', // 直接覆盖默认值
  },
};

这里的关键点:

  • argTypes 中的 control 配置让 Storybook 自动生成可视化控件,设计师无需看代码就能调试;
  • play 函数内使用 waitFor 等待状态更新,比 await new Promise(setTimeout) 更可靠;
  • Loading 故事通过 args 覆盖 apiStatus ,实现“同一组件,多状态预览”,避免重复写 story。

3.4 RTK Query 的自动 Mock 实现(深度解析)

这是让 Storybook 真正脱离后端依赖的核心。在 ReduxDecorator.tsx 中补充 mock 逻辑:

// .storybook/decorators/ReduxDecorator.tsx 续写
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

// 定义 mock 数据映射表
const MOCK_DATA_MAP: Record<string, any> = {
  '/api/users': { users: [{ id: 1, name: 'Mock User' }] },
  '/api/posts': { posts: [{ id: 1, title: 'Mock Post' }] },
};

// 创建 mock baseQuery
const mockBaseQuery = async (args: any) => {
  const { url } = args;
  if (MOCK_DATA_MAP[url]) {
    return { data: MOCK_DATA_MAP[url] };
  }
  // 默认返回空数据,避免报错
  return { data: {} };
};

// 在 Decorator 中动态替换 baseQuery
if (mockData && typeof window !== 'undefined') {
  // 劫持 RTK Query 的 createApi 配置
  const originalCreateApi = createApi;
  (createApi as any) = (...args: any[]) => {
    const config = args[0];
    // 用 mockBaseQuery 替换原始 baseQuery
    return originalCreateApi({
      ...config,
      baseQuery: mockBaseQuery,
    });
  };
}

实操心得:不要用 msw 在 Storybook 中 mock API!MSW 需要启动 service worker,而 Storybook 的 iframe 环境对 service worker 支持不稳定,常导致 mock 失效。直接在 baseQuery 层拦截,100% 可靠,且无需额外依赖。

4. 完整实操流程:从创建到发布,每一步踩坑记录

4.1 初始化 Storybook 配置文件

创建 .storybook/main.ts

import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
    '@storybook/addon-mdx-gfm',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  // 关键:禁用 Vite 的 HMR 冲突插件
  features: {
    interactionsDebugger: true,
  },
  // 自定义 Vite 配置(适配 RTK)
  viteFinal: async (config) => {
    // 确保 Vite 解析 .mjs 文件(RTK 依赖)
    if (!config.resolve?.extensions) {
      config.resolve = { extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'] };
    }
    return config;
  },
};

export default config;

创建 .storybook/preview.ts (全局预览配置):

import type { Preview } from '@storybook/react';
import { withThemeByClassName } from '@storybook/addon-styling';
import '../src/index.css'; // 全局 CSS

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
  // 应用全局 Decorator(所有故事默认启用 Redux)
  decorators: [
    withThemeByClassName({
      themes: {
        light: 'light',
        dark: 'dark',
      },
      defaultTheme: 'light',
    }),
  ],
};

export const globalTypes = {
  // 全局主题开关(可选)
  theme: {
    name: 'Theme',
    description: 'Global theme for components',
    defaultValue: 'light',
    toolbar: {
      icon: 'circlehollow',
      items: [
        { value: 'light', title: 'Light' },
        { value: 'dark', title: 'Dark' },
      ],
    },
  },
};

export default preview;

4.2 解决 Vite + Storybook 的 HMR 冲突(血泪教训)

这是我在三个项目中反复踩的坑。现象:修改 Button.tsx 后,Storybook 页面不刷新,必须手动 F5。根源是 Vite 的 HMR 插件与 Storybook 的事件监听冲突。

解决方案(亲测有效):

  1. vite.config.ts 中添加:
export default defineConfig({
  // ...其他配置
  plugins: [
    // 确保 Storybook 插件在最后
    ...(process.env.STORYBOOK === 'true' ? [] : [react()]),
  ],
});
  1. .storybook/main.ts viteFinal 中显式关闭 Storybook 的 HMR 插件:
viteFinal: async (config) => {
  // 移除可能冲突的插件
  config.plugins = config.plugins?.filter(
    (p) => !p?.name?.includes('hmr') && !p?.name?.includes('hot')
  );
  return config;
},
  1. 启动命令改为:
# package.json
"scripts": {
  "storybook": "STORYBOOK=true start-storybook -p 6006"
}

注意: STORYBOOK=true 环境变量是关键,它让 Vite 在 Storybook 模式下跳过自己的 HMR 插件,只用 Storybook 的热更新机制。

4.3 构建生产环境 Storybook(发布到宝塔/静态服务器)

网络热词里“如何把react项目发布到宝塔上”很常见,Storybook 同理。执行:

# 构建静态站点
npm run build-storybook

# 输出目录:storybook-static/
# 将此目录上传至宝塔的网站根目录即可

但要注意两个坑:

  • 路由问题 :若部署在子路径(如 https://example.com/storybook/ ),需在 .storybook/main.ts 中配置:
// .storybook/main.ts
export default config: StorybookConfig = {
  // ...
  staticDirs: ['../public'], // 静态资源目录
  // 添加此行(路径末尾必须有 /)
  basePath: '/storybook/',
};
  • CSS 作用域污染 :Storybook 默认会注入全局 CSS,可能影响主站样式。解决方案是在 preview.ts 中添加:
// .storybook/preview.ts
import { addDecorator } from '@storybook/react';
import { withThemeFromJSXProvider } from '@storybook/addon-styling';

addDecorator(
  withThemeFromJSXProvider({
    themes: {
      light: { /* light theme */ },
      dark: { /* dark theme */ },
    },
    defaultTheme: 'light',
  })
);

4.4 与 React 18 新特性深度集成

React 18 的 createRoot 和并发特性对 Storybook 有影响。在 preview.ts 中确保:

// .storybook/preview.ts
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

// Storybook 7+ 默认使用 createRoot
const root = createRoot(document.getElementById('root')!);
root.render(
  <StrictMode>
    <Story />
  </StrictMode>
);

同时,在 Button.stories.tsx 中,若组件使用了 useTransition startTransition ,需在 play 函数中等待过渡完成:

// play 函数内
await waitFor(() => {
  expect(button).toHaveClass('transitioning');
}, { timeout: 2000 }); // 设置超时

5. 常见问题与排查技巧实录

5.1 典型问题速查表

问题现象 可能原因 解决方案
Storybook 启动报错 Cannot find module 'react/jsx-runtime' Vite 版本与 React 18 不兼容 升级 vite 到 4.2+, @vitejs/plugin-react 到 4.0+
修改 story 文件后页面不刷新 HMR 插件冲突(见 4.2 节) 按 4.2 节方案禁用冲突插件
useSelector 返回 undefined RootState 类型未正确导出或导入 检查 store/index.ts export type RootState = ReturnType<typeof store.getState>;
RTK Query 请求未被 mock mockBaseQuery 未生效 ReduxDecorator console.log('mock active') 确认执行路径
Button 点击后 UI 不更新 dispatch 未触发 re-render 确保 useSelector 的 selector 返回新引用(用 shallowEqual reselect

5.2 独家避坑技巧

技巧1:用 args 模拟复杂嵌套状态
当 state 结构很深(如 user.profile.settings.theme ),不要在 argTypes 中定义多层 control。改用 JSON 字符串输入:

argTypes: {
  initialStateJson: {
    control: { type: 'text' },
    description: '初始 state JSON(覆盖整个 preloadedState)',
  }
}

然后在 Decorator 中 JSON.parse(args.initialStateJson) ,灵活度极高。

技巧2:一键生成所有状态组合的故事
为避免手动写 Primary , Loading , Error 等多个 story,用 forEach 自动生成:

// Button.stories.tsx
const STATES = [
  { userRole: 'admin', apiStatus: 'idle', name: 'Admin Idle' },
  { userRole: 'guest', apiStatus: 'pending', name: 'Guest Loading' },
  { userRole: 'editor', apiStatus: 'failed', name: 'Editor Failed' },
];

STATES.forEach(({ userRole, apiStatus, name }) => {
  export const [name]: Story = {
    args: { userRole, apiStatus },
  };
});

技巧3:在 Storybook 中调试 Redux DevTools
.storybook/preview.ts 中添加:

import { composeWithDevTools } from '@reduxjs/toolkit';

// 在 configureStore 中启用
const store = configureStore({
  reducer: rootReducer,
  devTools: process.env.NODE_ENV !== 'production',
  // ...其他配置
});

然后打开浏览器 Redux DevTools,选择 storybook 实例即可调试。

5.3 性能优化:大型项目必做三件事

  1. 按需加载 stories
    .storybook/main.ts 中,用 glob 模式排除无关文件:
stories: [
  '../src/components/**/Button.stories.@(js|jsx|ts|tsx)',
  '../src/features/**/Header.stories.@(js|jsx|ts|tsx)',
],
  1. 禁用非必要 addon
    @storybook/addon-a11y 在开发期很重,建议只在 CI 中运行:
// .storybook/main.ts
addons: [
  '@storybook/addon-essentials',
  // process.env.CI ? '@storybook/addon-a11y' : '',
]
  1. 缓存 store 创建
    对纯展示型组件(无 dispatch),在 Decorator 中加缓存:
// .storybook/decorators/ReduxDecorator.tsx
let cachedStore: Store | null = null;
let lastArgsHash = '';

const getStore = (args: ReduxDecoratorProps) => {
  const hash = JSON.stringify(args);
  if (hash === lastArgsHash && cachedStore) {
    return cachedStore;
  }
  cachedStore = createStore(/* ... */);
  lastArgsHash = hash;
  return cachedStore;
};

6. 进阶扩展:让 Storybook 成为你的前端质量中枢

6.1 集成 Cypress 实现视觉回归测试

网络热词里“前端react面试考察代码”常涉及自动化测试。Storybook + Cypress 是黄金组合:

npm install -D cypress @cypress/react @cypress/webpack-dev-server

cypress/e2e/storybook.cy.ts 中:

import { mount } from '@cypress/react';
import { Button } from '../../src/components/Button';

describe('Button Visual Regression', () => {
  it('matches snapshot in admin role', () => {
    mount(<Button onClick={() => {}} children="Admin" />);
    cy.percySnapshot('Button - Admin');
  });

  it('matches snapshot in guest role', () => {
    // 使用 ReduxDecorator 的 args 模拟 guest
    mount(
      <ReduxDecorator userRole="guest">
        <Button onClick={() => {}} children="Guest" />
      </ReduxDecorator>
    );
    cy.percySnapshot('Button - Guest');
  });
});

Percy 会自动生成像素级对比图,任何 UI 变更都会触发告警。

6.2 用 Storybook Docs 自动生成组件文档

.storybook/preview.ts 中启用 Docs:

import { addParameters } from '@storybook/react';

addParameters({
  docs: {
    page: () => (
      <>
        <Title />
        <Subtitle />
        <Description />
        <Primary />
        <ArgsTable story="*" />
        <Stories />
      </>
    ),
  },
});

然后在 Button.stories.tsx 中添加 MDX:

import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { Button } from './Button';

<Meta title="Components/Button" component={Button} />

# Button

用于触发主要操作的按钮组件。

## Props

<ArgsTable story="*" />

## Usage

```tsx
<Button onClick={() => console.log('clicked')}>Click me</Button>

运行 `npm run build-storybook` 后,`storybook-static/` 中会生成完整文档站点,含代码示例、Props 表、实时预览。

### 6.3 与 React Flow 集成(面向复杂状态流)

若你的组件涉及状态机(如订单流程),可结合 `react-flow` 可视化状态流转:

```tsx
// Button.stories.tsx
import ReactFlow, { Controls } from 'react-flow-renderer';

export const StateFlow: Story = {
  render: () => (
    <div style={{ height: 400 }}>
      <ReactFlow elements={flowElements}>
        <Controls />
      </ReactFlow>
    </div>
  ),
};

其中 flowElements 可根据 userRole apiStatus 动态生成节点,直观展示“guest 点击 → 触发 login → 跳转 auth 页面”的完整链路。


我在实际使用中发现,真正让 Storybook 发挥最大价值的,不是它能渲染多少组件,而是 它迫使团队建立一套共同语言 :设计师用 Controls 调整参数,后端用 Docs 查看 Props 接口,测试同学用 Interactions 录制操作流,新人通过 Stories 10 分钟掌握所有状态边界。去年我们一个 20 人团队,上线前 Bug 率下降 63%,核心就是把 Storybook 从“锦上添花的文档工具”,变成了“所有代码必须通过的准入关卡”。

最后再分享一个小技巧:在 Button.stories.tsx play 函数里,不要只验证 UI 变化,加上一行 console.log('✅ Button state test passed') 。当 CI 环境跑 Storybook 测试时,这行日志会成为你快速定位失败用例的救命稻草——毕竟,在凌晨三点收到告警邮件时,你最需要的不是华丽的报告,而是一句清晰的“哪里坏了”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值