组装一个复合组件
上一章我们构建了第一个组件;本章扩展了我们所学的知识,以构建 TaskList,一个任务列表。让我们将组件组合在一起,看看引入更多复杂性时会发生什么。
任务列表
Taskbox 通过将固定的任务放置在默认任务之上来突出显示它们。这产生了您需要为其创建故事的 TaskList
的两种变体:默认项目以及默认项目和固定项目。
由于 Task
数据可以异步发送,因此我们还需要一个加载状态,以便在没有连接时进行渲染。此外,当没有任务时,还需要一个空状态。
开始设置
复合组件与其包含的基本组件没有太大区别。创建一个 TaskList
组件和一个配套的故事文件:components/TaskList.jsx
和 components/TaskList.stories.jsx
。
从 TaskList
的粗略实现开始。您需要从前面导入 Task
组件,并将属性和操作作为输入传入。
import { Task } from './Task';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { styles } from './styles';
export const TaskList = ({ loading, tasks, onPinTask, onArchiveTask }) => {
const events = {
onPinTask,
onArchiveTask,
};
if (loading) {
return (
<View style={styles.listItems}>
<Text>loading</Text>
</View>
);
}
if (tasks.length === 0) {
return (
<View style={styles.listItems}>
<Text>empty</Text>
</View>
);
}
return (
<View style={styles.listItems}>
<FlatList
data={tasks}
keyExtractor={(task) => task.id}
renderItem={({ item }) => (
<Task key={item.id} task={item} {...events} />
)}
/>
</View>
);
};
接下来在故事文件中创建 Tasklist
的测试状态。
import { TaskList } from './TaskList';
import { Default as TaskStory } from './Task.stories';
import { View } from 'react-native';
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 = {
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 = {
args: {
tasks: [
...Default.args.tasks.slice(0, 5),
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
],
},
};
export const Loading = {
args: {
tasks: [],
loading: true,
},
};
export const Empty = {
args: {
// Shaping the stories through args composition.
// Inherited data coming from the Loading story.
...Loading.args,
loading: false,
},
};
装饰器 是一种为故事提供任意包装器的方法。在本例中,我们使用装饰器在列表周围添加填充,以便更轻松地进行视觉验证。它们还可以用于将故事包装在“提供程序”中——即设置 React 上下文的库组件。
TaskStory.args.task
提供了我们从 Task.stories.js
文件创建和导出的 Task
的形状。同样,我们为 onPinTask
和 onArchiveTask
添加的 argTypes
告诉 Storybook 提供 TaskList
组件所需的操作(模拟回调)。
如果您没有立即看到新的故事,请尝试重新加载应用程序。如果这不起作用,您可以重新运行 yarn storybook-generate
以重新生成 storybook.requires
文件。
现在检查 Storybook 中新的 TaskList
故事。
构建状态
我们的组件仍然很粗糙,但现在我们对要实现的故事有了一个概念。您可能会认为 listitems
包装器过于简单。您是对的——在大多数情况下,我们不会仅仅为了添加包装器而创建一个新组件。但是 TaskList
组件的真正复杂性在边缘情况下揭示出来:withPinnedTasks
、loading
和 empty
。
对于加载情况,我们将创建一个新组件来显示加载动画。
创建一个名为 LoadingRow.jsx
的新文件,其中包含以下内容
import { useState, useEffect } from 'react';
import { Animated, Text, View, Easing, StyleSheet } from 'react-native';
import { styles } from './styles';
const GlowView = ({ style, children }) => {
const [glowAnim] = useState(new Animated.Value(0.3));
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(glowAnim, {
toValue: 1,
duration: 1500,
easing: Easing.ease,
useNativeDriver: true,
}),
Animated.timing(glowAnim, {
toValue: 0.3,
duration: 1500,
easing: Easing.ease,
useNativeDriver: true,
}),
])
).start();
}, []);
return (
<Animated.View
style={{
...style,
opacity: glowAnim,
}}
>
{children}
</Animated.View>
);
};
export const LoadingRow = () => (
<View style={styles.container}>
<GlowView>
<View style={styles.loadingItem}>
<View style={styles.glowCheckbox} />
<Text style={styles.glowText}>Loading</Text>
<Text style={styles.glowText}>cool</Text>
<Text style={styles.glowText}>state</Text>
</View>
</GlowView>
</View>
);
并将 TaskList.jsx
更新为以下内容
import { Task } from './Task';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { LoadingRow } from './LoadingRow';
import { MaterialIcons } from '@expo/vector-icons';
import { styles } from './styles';
export const TaskList = ({ loading, tasks, onPinTask, onArchiveTask }) => {
const events = {
onPinTask,
onArchiveTask,
};
if (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>
);
}
const tasksInOrder = [
...tasks.filter((t) => t.state === 'TASK_PINNED'),
...tasks.filter((t) => t.state !== 'TASK_PINNED'),
];
return (
<View style={styles.listItems}>
<FlatList
data={tasksInOrder}
keyExtractor={(task) => task.id}
renderItem={({ item }) => (
<Task key={item.id} task={item} {...events} />
)}
/>
</View>
);
};
这些更改导致以下 UI
成功!我们完成了我们设定的目标。如果我们检查更新后的 UI,我们可以快速看到我们固定的任务现在显示在列表的顶部,与预期的设计相符。在接下来的章节中,我们将扩展我们所学的知识,并通过将这些原则应用于更复杂的 UI 来继续增加应用程序的复杂性。