接入数据
到目前为止,我们创建了独立的无状态组件——这对于 Storybook 来说很棒,但在我们的应用中赋予它们一些数据之前,它们最终无法提供帮助。
本教程不专注于构建应用的具体细节,因此我们在此不会深入探讨这些细节。但我们将花点时间看看将数据接入连接组件的常见模式。
连接组件
我们目前编写的 TaskList
组件是“展示型”的,因为它不与其自身实现外部的任何内容进行交互。我们需要将其连接到数据提供程序才能获取数据。
本例使用 Redux Toolkit(与 Redux 一起开发应用以存储数据的最有效工具集),为我们的应用构建一个简单的数据模型。但是,这里使用的模式同样适用于其他数据管理库,如 Apollo 和 MobX。
使用以下命令为你的项目添加必要的依赖项
yarn add @reduxjs/toolkit react-redux
首先,我们将在项目根目录下的 store.js
文件中构建一个简单的 Redux 存储(store),它响应改变任务状态的 action。
/* A simple redux store/actions/reducer implementation.
* A true app would be more complex and separated into different files.
*/
import { configureStore, createSlice } from '@reduxjs/toolkit';
/*
* The initial state of our store when the app loads.
* Usually, you would fetch this from a server. Let's not worry about that now
*/
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' },
];
const TaskBoxData = {
tasks: defaultTasks,
status: 'idle',
error: null,
};
/*
* The store is created here.
* You can read more about Redux Toolkit's slices in the docs:
* https://toolkit.redux.js.cn/api/createSlice
*/
const TasksSlice = createSlice({
name: 'taskbox',
initialState: TaskBoxData,
reducers: {
updateTaskState: (state, action) => {
const { id, newTaskState } = action.payload;
const task = state.tasks.findIndex((task) => task.id === id);
if (task >= 0) {
state.tasks[task].state = newTaskState;
}
},
},
});
// The actions contained in the slice are exported for usage in our components
export const { updateTaskState } = TasksSlice.actions;
/*
* Our app's store configuration goes here.
* Read more about Redux's configureStore in the docs:
* https://toolkit.redux.js.cn/api/configureStore
*/
const store = configureStore({
reducer: {
taskbox: TasksSlice.reducer,
},
});
export default store;
然后,我们将更新我们的 TaskList
组件,使其连接到 Redux 存储并渲染我们感兴趣的任务。
import { Task } from './Task';
import { FlatList, Text, View } from 'react-native';
import { LoadingRow } from './LoadingRow';
import { MaterialIcons } from '@expo/vector-icons';
import { styles } from './styles';
import { useDispatch, useSelector } from 'react-redux';
import { updateTaskState } from '../store';
export const TaskList = () => {
// We're retrieving our state from the store
const tasks = useSelector((state) => {
const tasksInOrder = [
...state.taskbox.tasks.filter((t) => t.state === 'TASK_PINNED'),
...state.taskbox.tasks.filter((t) => t.state !== 'TASK_PINNED'),
];
const filteredTasks = tasksInOrder.filter(
(t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED'
);
return filteredTasks;
});
const { status } = useSelector((state) => state.taskbox);
const dispatch = useDispatch();
const pinTask = (value) => {
// We're dispatching the Pinned event back to our store
dispatch(updateTaskState({ id: value, newTaskState: 'TASK_PINNED' }));
};
const archiveTask = (value) => {
// We're dispatching the Archive event back to our store
dispatch(updateTaskState({ id: value, newTaskState: 'TASK_ARCHIVED' }));
};
if (status === "loading") {
return (
<View style={[styles.listItems, { justifyContent: "center" }]}>
<LoadingRow />
<LoadingRow />
<LoadingRow />
<LoadingRow />
<LoadingRow />
<LoadingRow />
</View>
);
}
if (tasks.length === 0) {
return (
<View style={styles.listItems}>
<View style={styles.wrapperMessage}>
<MaterialIcons name="check" size={64} color={"#2cc5d2"} />
<Text style={styles.titleMessage}>You have no tasks</Text>
<Text style={styles.subtitleMessage}>Sit back and relax</Text>
</View>
</View>
);
}
return (
<View style={styles.listItems}>
<FlatList
data={tasks}
keyExtractor={(task) => task.id}
renderItem={({ item }) => (
<Task
key={item.id}
task={item}
onPinTask={(task) => pinTask(task)}
onArchiveTask={(task) => archiveTask(task)}
/>
)}
/>
</View>
);
};
现在我们有一些实际数据填充我们的组件,这些数据来自 Redux 存储,我们可以将其连接到我们的应用 App.jsx
并在那里渲染组件。但现在,让我们先不要这样做,而是继续我们的组件驱动之旅。
使用装饰器提供上下文
由于我们的 Tasklist
现在是一个连接组件(因为它依赖于 Redux 存储来检索和更新我们的任务),Storybook 故事因这一更改而停止工作。

我们可以使用各种方法来解决这个问题。不过,由于我们的应用非常简单,我们可以像在上一章中所做的那样,依赖于一个装饰器,并在 Storybook 故事中提供一个模拟的存储。
import { TaskList } from './TaskList';
import { Default as TaskStory } from './Task.stories';
import { View } from 'react-native';
import { Provider } from 'react-redux';
import { configureStore, createSlice } from '@reduxjs/toolkit';
// A super-simple mock of the state of the store
const MockedState = {
tasks: [
{ ...TaskStory.args.task, id: '1', title: 'Task 1' },
{ ...TaskStory.args.task, id: '2', title: 'Task 2' },
{ ...TaskStory.args.task, id: '3', title: 'Task 3' },
{ ...TaskStory.args.task, id: '4', title: 'Task 4' },
{ ...TaskStory.args.task, id: '5', title: 'Task 5' },
{ ...TaskStory.args.task, id: '6', title: 'Task 6' },
],
status: 'idle',
error: null,
};
// A super-simple mock of a redux store
const Mockstore = ({ taskboxState, children }) => (
<Provider
store={configureStore({
reducer: {
taskbox: createSlice({
name: 'taskbox',
initialState: taskboxState,
reducers: {
updateTaskState: (state, action) => {
const { id, newTaskState } = action.payload;
const task = state.tasks.findIndex((task) => task.id === id);
if (task >= 0) {
state.tasks[task].state = newTaskState;
}
},
},
}).reducer,
},
})}
>
{children}
</Provider>
);
export default {
component: TaskList,
title: 'TaskList',
decorators: [
(Story) => (
<View style={{ padding: 42, flex: 1 }}>
<Story />
</View>
),
],
argTypes: {
onPinTask: { action: 'onPinTask' },
onArchiveTask: { action: 'onArchiveTask' },
},
};
export const Default = {
decorators: [
(story) => <Mockstore taskboxState={MockedState}>{story()}</Mockstore>,
],
args: {
// Shaping the stories through args composition.
// The data was inherited from the Default story in Task.stories.js.
tasks: [
{ ...TaskStory.args.task, id: '1', title: 'Task 1' },
{ ...TaskStory.args.task, id: '2', title: 'Task 2' },
{ ...TaskStory.args.task, id: '3', title: 'Task 3' },
{ ...TaskStory.args.task, id: '4', title: 'Task 4' },
{ ...TaskStory.args.task, id: '5', title: 'Task 5' },
{ ...TaskStory.args.task, id: '6', title: 'Task 6' },
],
},
};
export const WithPinnedTasks = {
decorators: [
(story) => {
const pinnedtasks = [
...MockedState.tasks.slice(0, 5),
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
];
return (
<Mockstore
taskboxState={{
...MockedState,
tasks: pinnedtasks,
}}
>
{story()}
</Mockstore>
);
},
],
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 = {
decorators: [
(story) => (
<Mockstore
taskboxState={{
...MockedState,
status: 'loading',
}}
>
{story()}
</Mockstore>
),
],
args: {
tasks: [],
loading: true,
},
};
export const Empty = {
decorators: [
(story) => (
<Mockstore
taskboxState={{
...MockedState,
tasks: [],
}}
>
{story()}
</Mockstore>
),
],
args: {
// Shaping the stories through args composition.
// Inherited data coming from the Loading story.
...Loading.args,
loading: false,
},
};
成功!我们回到了起点,我们的 Storybook 现在可以正常工作了,我们也可以看到如何将数据提供给连接组件。