連結資料
了解把資料連結到 UI 元件的方法
此社群翻譯還未更新至最新版本的 Storybook。請透過英文版本的更變,協助我們更新此翻譯。 歡迎發送 Pull Request.
目前,我們已經做好沒有狀態的獨立元件:對 Storybook 來說很夠用。但到頭來,在 App 加入資料之前是沒什麼用處的。
這份教學並不是要專注鑽研 App 製作,因此不會深入探討細節。但仍會花點時間檢視將容器元件 (container components) 接上資料的常見模式。
容器元件
TaskList
元件現在的寫法是「展示狀態」(可以看這篇文章),因為並沒有進行任何外部溝通,將其接到自身的作法。要灌入資料,就得有個「容器」。
這裡儲存資料的範例,使用最受歡迎的 React 函式庫:Redux,來打造簡易的 App 資料 model。不過,這裡使用的模式也,可以好好的用在其它資料管理函式庫,像是 Apollo 和 MobX。
在專案裡加入要用到的相依套件:
yarn add react-redux redux
一開始,在 src
資料夾裡,加入 lib/redux.js
這支檔案(有刻意簡化),蓋出簡單的 Redux store,對應改變任務狀態的 Action。
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,並渲染出目標任務。
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 的宣告,就可以了:
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 提交改好的東西!