添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
Back to Intro to Storybook

连线数据

了解如何将数据连接到UI组件
此社区翻译尚未更新为最新的Storybook版本。通过应用此翻译的中文指南中的更改来帮助我们更新它。 Pull requests 欢迎他们 .

到目前为止,我们创建了孤立的无状态组件 - Storybook 很棒,但作用不大,除非我们在应用程序中为他们提供一些数据.

本教程不关注构建应用程序的细节,因此我们不会在此处深入研究这些细节. 但我们将花点时间研究一下 与容器组件 连接数据 的常见模式.

我们的 TaskList 目前编写的组件是"表现性的" (见 这篇博文 ) 因为它不会与 其自身实现之外 的任何内容交谈. 为了获取数据,我们需要一个"容器".

这个例子使用 Redux ,最流行的 React 库,用于存储数据,为我们的应用程序构建一个简单的数据模型. 但是,此处使用的模式同样适用于其他数据管理库 阿波罗 MobX .

在你的项目中添加必要的依赖:

yarn add react-redux redux

首先,我们将构建一个简单的 Redux 存储,它在一个src/lib/redux.js中定义改变任务状态的操作 (故意保持简单) :

src/lib/redux.js
// A simple redux store/actions/reducer implementation.
// A true app would be more complex and separated into different files.
import { createStore } from 'redux';
// The actions are the "names" of the changes that can happen to the store
export const actions = {
  ARCHIVE_TASK: 'ARCHIVE_TASK',
  PIN_TASK: 'PIN_TASK',
// The action creators bundle actions with the data required to execute them
export const archiveTask = (id) => ({ type: actions.ARCHIVE_TASK, id });
export const pinTask = (id) => ({ type: actions.PIN_TASK, id });
// All our reducers simply change the state of a single task.
function taskStateReducer(taskState) {
  return (state, action) => {
    return {
      ...state,
      tasks: state.tasks.map((task) =>
        task.id === action.id ? { ...task, state: taskState } : task
// The reducer describes how the contents of the store change for each action
export const reducer = (state, action) => {
  switch (action.type) {
    case actions.ARCHIVE_TASK:
      return taskStateReducer('TASK_ARCHIVED')(state, action);
    case actions.PIN_TASK:
      return taskStateReducer('TASK_PINNED')(state, action);
    default:
      return state;
// The initial state of our store when the app loads.
// Usually you would fetch this from a server
const defaultTasks = [
  { id: '1', title: 'Something', state: 'TASK_INBOX' },
  { id: '2', title: 'Something more', state: 'TASK_INBOX' },
  { id: '3', title: 'Something else', state: 'TASK_INBOX' },
  { id: '4', title: 'Something again', state: 'TASK_INBOX' },
// We export the constructed redux store
export default createStore(reducer, { tasks: defaultTasks });

然后我们将更新默认导出TaskList组件连接到 Redux 存储,并呈现我们感兴趣的任务:

src/components/TaskList.js
import React from 'react';
import PropTypes from 'prop-types';
import Task from './Task';
import { connect } from 'react-redux';
import { archiveTask, pinTask } from '../lib/redux';
export function PureTaskList({ loading, tasks, onPinTask, onArchiveTask }) {
  /* 以前的 TaskList 实现 */
PureTaskList.propTypes = {
  /** Checks if it's in loading state */
  loading: PropTypes.bool,
  /** The list of tasks */
  tasks: PropTypes.arrayOf(Task.propTypes.task).isRequired,
  /** Event to change the task to pinned */
  onPinTask: PropTypes.func.isRequired,
  /** Event to change the task to archived */
  onArchiveTask: PropTypes.func.isRequired,
PureTaskList.defaultProps = {
  loading: false,
export default connect(
  ({ tasks }) => ({
    tasks: tasks.filter((t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED'),
  (dispatch) => ({
    onArchiveTask: (id) => dispatch(archiveTask(id)),
    onPinTask: (id) => dispatch(pinTask(id)),
)(PureTaskList);

现在我们已经有了一些从 Redux 获得的用于填充组件的真实数据,我们可以连到 src/app.js 并在那渲染组件。但是现在我们先不这样做,让我们继续组件驱动之旅。

不要担心,我们将在下一章进行介绍。

在这个阶段,我们的 Storybook 测试将停止工作,因为TaskList现在是一个容器,不再需要任何 props,而是连接到 Store 并设置PureTaskList包裹组件的 props.

但是,我们可以通过简单地渲染PureTaskList来轻松解决这个问题 - 我们的 Storybook 故事中的表现部分:

src/components/TaskList.stories.js
import React from 'react'; import { PureTaskList } from './TaskList'; import * as TaskStories from './Task.stories'; export default { component: PureTaskList, title: 'TaskList', decorators: [(story) => <div style={{ padding: '3rem' }}>{story()}</div>], const Template = (args) => <PureTaskList {...args} />; export const Default = Template.bind({}); Default.args = { // Shaping the stories through args composition. // The data was inherited the Default story in task.stories.js. tasks: [ { ...TaskStories.Default.args.task, id: '1', title: 'Task 1' }, { ...TaskStories.Default.args.task, id: '2', title: 'Task 2' }, { ...TaskStories.Default.args.task, id: '3', title: 'Task 3' }, { ...TaskStories.Default.args.task, id: '4', title: 'Task 4' }, { ...TaskStories.Default.args.task, id: '5', title: 'Task 5' }, { ...TaskStories.Default.args.task, id: '6', title: 'Task 6' }, export const WithPinnedTasks = Template.bind({}); WithPinnedTasks.args = { // Shaping the stories through args composition. // Inherited data coming from the Default story. tasks: [ ...Default.args.tasks.slice(0, 5), { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' }, export const Loading = Template.bind({}); Loading.args = { tasks: [], loading: true, export const Empty = Template.bind({}); Empty.args = { // Shaping the stories through args composition. // Inherited data coming from the Loading story. ...Loading.args, loading: false,