Back to Intro to Storybook
Chapters
  • 開始
  • 簡易元件
  • 複合元件
  • 資料
  • 頁面
  • 部署
  • 進行測試
  • 外掛
  • 總結
  • 參與

連結資料

了解把資料連結到 UI 元件的方法
此社群翻譯還未更新至最新版本的 Storybook。請透過英文版本的更變,協助我們更新此翻譯。 歡迎發送 Pull Request.

目前,我們已經做好沒有狀態的獨立元件:對 Storybook 來說很夠用。但到頭來,在 App 加入資料之前是沒什麼用處的。

這份教學並不是要專注鑽研 App 製作,因此不會深入探討細節。但仍會花點時間檢視將容器元件 (container components) 接上資料的常見模式。

容器元件

TaskList 元件現在的寫法是「展示狀態」(可以看這篇文章),因為並沒有進行任何外部溝通,將其接到自身的作法。要灌入資料,就得有個「容器」。

這裡儲存資料的範例,使用最受歡迎的 React 函式庫:Redux,來打造簡易的 App 資料 model。不過,這裡使用的模式也,可以好好的用在其它資料管理函式庫,像是 ApolloMobX

在專案裡加入要用到的相依套件:

Copy
yarn add react-redux redux

一開始,在 src 資料夾裡,加入 lib/redux.js 這支檔案(有刻意簡化),蓋出簡單的 Redux store,對應改變任務狀態的 Action。

Copy
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 元件的預設 export,用來連接 Redux store,並渲染出目標任務。

Copy
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 }) {
  /* previous implementation of 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,在那邊渲染元件。但現在先不要,繼續在元件驅動的旅程。

不用擔心,下個章節就會來關照這邊。

這時候,因為 TaskList 現在是容器,沒有設定好接收任何 props,造成 Storybook 的測試無法運作。TaskList 現在連接至 store,改在它裡面的 PureTaskList設定 props。

然而,只要把 PureTaskList 這個展示元件渲染至上個步驟中,在 Storybook 裡 story 剛加入 export 的宣告,就可以了:

Copy
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,
};
💡 進行這些變動後,快照得要進行更新。重新執行一次測試指令,加上 -u 來更新。還有,別忘了在 git 提交改好的東西!
Keep your code in sync with this chapter. View c70ec15 on GitHub.
Is this free guide helping you? Tweet to give kudos and help other devs find it.
Next Chapter
頁面
以元件建構頁面
✍️ Edit on GitHub – PRs welcome!
Join the community
6,616 developers and counting
WhyWhy StorybookComponent-driven UI
Open source software
Storybook

Maintained by
Chromatic
Special thanks to Netlify and CircleCI