React 交互性:编辑、过滤、条件渲染
随着我们的 React 之旅接近尾声(至少目前是这样),我们将为待办事项列表应用的主要功能区域添加点睛之笔。这包括允许您编辑现有任务,以及在所有任务、已完成任务和未完成任务之间过滤任务列表。在此过程中,我们将探讨条件 UI 渲染。
| 预备知识 | 熟悉核心 HTML、CSS 和 JavaScript 语言,以及 终端/命令行。 |
|---|---|
| 学习成果 | React 中的条件渲染,以及在我们的应用程序中实现列表过滤和编辑 UI。 |
编辑任务名称
我们还没有一个用于编辑任务名称的用户界面。我们稍后会讲到。首先,我们至少可以在 App.jsx 中实现一个 editTask() 函数。它会类似于 deleteTask(),因为它会接受一个 id 来查找其目标对象,但它还会接受一个 newName 属性,其中包含要更新为的任务名称。我们将使用 Array.prototype.map() 而不是 Array.prototype.filter(),因为我们想要返回一个包含一些更改的新数组,而不是从数组中删除某些内容。
在您的 <App /> 组件内部,与其他函数相同的位置添加 editTask() 函数。
function editTask(id, newName) {
const editedTaskList = tasks.map((task) => {
// if this task has the same ID as the edited task
if (id === task.id) {
// Copy the task and update its name
return { ...task, name: newName };
}
// Return the original task if it's not the edited task
return task;
});
setTasks(editedTaskList);
}
以与 deleteTask 相同的方式,将 editTask 作为 prop 传递到我们的 <Todo /> 组件中。
const taskList = tasks.map((task) => (
<Todo
id={task.id}
name={task.name}
completed={task.completed}
key={task.id}
toggleTaskCompleted={toggleTaskCompleted}
deleteTask={deleteTask}
editTask={editTask}
/>
));
现在打开 Todo.jsx。我们将进行一些重构。
编辑 UI
为了允许用户编辑任务,我们必须为他们提供一个用户界面来执行此操作。首先,像我们之前在 <App /> 组件中那样,将 useState 导入到 <Todo /> 组件中。
import { useState } from "react";
我们将用它来设置一个 isEditing 状态,默认值为 false。在您的 <Todo /> 组件定义顶部内部添加以下行。
const [isEditing, setEditing] = useState(false);
接下来,我们将重新考虑 <Todo /> 组件。从现在开始,我们希望它显示两个可能的“模板”之一,而不是到目前为止使用的单个模板。
- “查看”模板,当我们只是查看待办事项时;这是我们到目前为止在教程中使用的。
- “编辑”模板,当我们正在编辑待办事项时。我们即将创建它。
将此代码块复制到 Todo() 函数中,放在 useState() 钩子下方但 return 语句上方。
const editingTemplate = (
<form className="stack-small">
<div className="form-group">
<label className="todo-label" htmlFor={props.id}>
New name for {props.name}
</label>
<input id={props.id} className="todo-text" type="text" />
</div>
<div className="btn-group">
<button type="button" className="btn todo-cancel">
Cancel
<span className="visually-hidden">renaming {props.name}</span>
</button>
<button type="submit" className="btn btn__primary todo-edit">
Save
<span className="visually-hidden">new name for {props.name}</span>
</button>
</div>
</form>
);
const viewTemplate = (
<div className="stack-small">
<div className="c-cb">
<input
id={props.id}
type="checkbox"
defaultChecked={props.completed}
onChange={() => props.toggleTaskCompleted(props.id)}
/>
<label className="todo-label" htmlFor={props.id}>
{props.name}
</label>
</div>
<div className="btn-group">
<button type="button" className="btn">
Edit <span className="visually-hidden">{props.name}</span>
</button>
<button
type="button"
className="btn btn__danger"
onClick={() => props.deleteTask(props.id)}>
Delete <span className="visually-hidden">{props.name}</span>
</button>
</div>
</div>
);
我们现在已经将两种不同的模板结构(“编辑”和“视图”)定义在两个单独的常量中。这意味着 <Todo /> 的 return 语句现在是重复的——它也包含“视图”模板的定义。我们可以通过使用条件渲染来清理这一点,以确定组件返回哪个模板,从而在 UI 中渲染。
条件渲染
在 JSX 中,我们可以使用条件来更改浏览器渲染的内容。要在 JSX 中编写条件,我们可以使用三元运算符。
在我们的 <Todo /> 组件的情况下,我们的条件是“此任务是否正在被编辑?”更改 Todo() 中的 return 语句,使其如下所示:
return <li className="todo">{isEditing ? editingTemplate : viewTemplate}</li>;
您的浏览器应该像以前一样渲染所有任务。要查看编辑模板,您现在必须在代码中将默认的 isEditing 状态从 false 更改为 true;我们将在下一节中探讨如何使编辑按钮切换此状态!
切换 <Todo /> 模板
终于,我们准备好让我们的最后一个核心功能具有交互性。首先,当用户按下我们 viewTemplate 中的“编辑”按钮时,我们希望调用 setEditing() 并将其值设置为 true,以便我们可以切换模板。
像这样更新 viewTemplate 中的“编辑”按钮
<button type="button" className="btn" onClick={() => setEditing(true)}>
Edit <span className="visually-hidden">{props.name}</span>
</button>
现在我们将向 editingTemplate 中的“取消”按钮添加相同的 onClick 处理程序,但这次我们将 isEditing 设置为 false,以便它将我们切换回视图模板。
像这样更新 editingTemplate 中的“取消”按钮
<button
type="button"
className="btn todo-cancel"
onClick={() => setEditing(false)}>
Cancel
<span className="visually-hidden">renaming {props.name}</span>
</button>
有了这段代码,您应该能够点击待办事项中的“编辑”和“取消”按钮来切换模板。


下一步是实际让编辑功能生效。
从 UI 中编辑
我们接下来要做的很多事情都将与我们在 Form.jsx 中所做的工作类似:当用户在我们的新输入字段中输入时,我们需要跟踪他们输入的文本;一旦他们提交表单,我们需要使用回调 prop 来使用任务的新名称更新我们的状态。
我们将首先创建一个新的 hook 来存储和设置新名称。仍在 Todo.jsx 中,将以下内容放在现有 hook 下面。
const [newName, setNewName] = useState("");
接下来,创建一个 handleChange() 函数来设置新名称;将其放在 hook 下方但在模板上方。
function handleChange(e) {
setNewName(e.target.value);
}
现在我们将更新 editingTemplate 的 <input /> 字段,设置 newName 的 value 属性,并将我们的 handleChange() 函数绑定到其 onChange 事件。如下更新它:
<input
id={props.id}
className="todo-text"
type="text"
value={newName}
onChange={handleChange}
/>
最后,我们需要创建一个函数来处理编辑表单的 onSubmit 事件。将以下内容添加到 handleChange() 正下方。
function handleSubmit(e) {
e.preventDefault();
props.editTask(props.id, newName);
setNewName("");
setEditing(false);
}
请记住,我们的 editTask() 回调 prop 需要我们正在编辑的任务的 ID 以及其新名称。
通过向 editingTemplate 的 <form> 添加以下 onSubmit 处理程序,将此函数绑定到表单的 submit 事件。
<form className="stack-small" onSubmit={handleSubmit}>
{/* … */}
</form>
您现在应该能够在浏览器中编辑任务了。此时,您的 Todo.jsx 文件应该如下所示:
function Todo(props) {
const [isEditing, setEditing] = useState(false);
const [newName, setNewName] = useState("");
function handleChange(e) {
setNewName(e.target.value);
}
function handleSubmit(e) {
e.preventDefault();
props.editTask(props.id, newName);
setNewName("");
setEditing(false);
}
const editingTemplate = (
<form className="stack-small" onSubmit={handleSubmit}>
<div className="form-group">
<label className="todo-label" htmlFor={props.id}>
New name for {props.name}
</label>
<input
id={props.id}
className="todo-text"
type="text"
value={newName}
onChange={handleChange}
/>
</div>
<div className="btn-group">
<button
type="button"
className="btn todo-cancel"
onClick={() => setEditing(false)}>
Cancel
<span className="visually-hidden">renaming {props.name}</span>
</button>
<button type="submit" className="btn btn__primary todo-edit">
Save
<span className="visually-hidden">new name for {props.name}</span>
</button>
</div>
</form>
);
const viewTemplate = (
<div className="stack-small">
<div className="c-cb">
<input
id={props.id}
type="checkbox"
defaultChecked={props.completed}
onChange={() => props.toggleTaskCompleted(props.id)}
/>
<label className="todo-label" htmlFor={props.id}>
{props.name}
</label>
</div>
<div className="btn-group">
<button
type="button"
className="btn"
onClick={() => {
setEditing(true);
}}>
Edit <span className="visually-hidden">{props.name}</span>
</button>
<button
type="button"
className="btn btn__danger"
onClick={() => props.deleteTask(props.id)}>
Delete <span className="visually-hidden">{props.name}</span>
</button>
</div>
</div>
);
return <li className="todo">{isEditing ? editingTemplate : viewTemplate}</li>;
}
export default Todo;
返回到过滤按钮
现在我们的主要功能已经完成,我们可以考虑我们的过滤按钮了。目前,它们重复了“全部”标签,并且没有任何功能!我们将重新运用我们在 <Todo /> 组件中使用的某些技能来
- 创建一个用于存储活动过滤器的 hook。
- 渲染一个
<FilterButton />元素数组,允许用户在全部、已完成和未完成任务之间更改活动过滤器。
添加过滤钩子
向您的 App() 函数添加一个新的 hook,用于读取和设置过滤器。我们希望默认过滤器是 All,因为所有任务都应该在初始时显示。
const [filter, setFilter] = useState("All");
定义我们的过滤器
我们目前的目标是双重的
- 每个过滤器都应该有一个唯一的名称。
- 每个过滤器都应该有一个独特的行为。
JavaScript 对象是关联名称和行为的好方法:每个键都是过滤器的名称;每个属性是与该名称关联的行为。
在 App.jsx 的顶部,在我们的导入下方但在 App() 函数上方,让我们添加一个名为 FILTER_MAP 的对象。
const FILTER_MAP = {
All: () => true,
Active: (task) => !task.completed,
Completed: (task) => task.completed,
};
FILTER_MAP 的值是我们将用于过滤 tasks 数据数组的函数。
All过滤器显示所有任务,因此我们对所有任务返回true。Active过滤器显示completed属性为false的任务。Completed过滤器显示completed属性为true的任务。
在之前添加的内容下方,添加以下内容——在这里我们使用 Object.keys() 方法来收集一个 FILTER_NAMES 数组。
const FILTER_NAMES = Object.keys(FILTER_MAP);
注意:我们将这些常量定义在 App() 函数之外,因为如果它们定义在函数内部,每次 <App /> 组件重新渲染时它们都会被重新计算,而我们不希望这样。无论我们的应用程序做什么,这些信息都不会改变。
渲染过滤器
现在我们有了 FILTER_NAMES 数组,我们可以用它来渲染所有三个过滤器。在 App() 函数内部,我们可以创建一个名为 filterList 的常量,我们将用它来遍历名称数组并返回一个 <FilterButton /> 组件。记住,这里也需要 keys。
在您的 taskList 常量声明下方添加以下内容
const filterList = FILTER_NAMES.map((name) => (
<FilterButton key={name} name={name} />
));
现在我们将用这个 filterList 替换 App.jsx 中重复的三个 <FilterButton />。替换以下内容:
<div className="filters btn-group stack-exception">
<FilterButton />
<FilterButton />
<FilterButton />
</div>
使用这个
<div className="filters btn-group stack-exception">{filterList}</div>
这还行不通。我们还有一些工作要做。
交互式过滤器
为了使我们的过滤按钮具有交互性,我们应该考虑它们需要利用哪些 props。
- 我们知道
<FilterButton />应该报告它当前是否被按下,如果它的名称与我们过滤器状态的当前值匹配,它就应该被按下。 - 我们知道
<FilterButton />需要一个回调来设置活动过滤器。我们可以直接使用我们的setFilterhook。
如下更新您的 filterList 常量
const filterList = FILTER_NAMES.map((name) => (
<FilterButton
key={name}
name={name}
isPressed={name === filter}
setFilter={setFilter}
/>
));
与我们之前处理 <Todo /> 组件的方式相同,现在我们必须更新 FilterButton.jsx 以利用我们提供的 prop。执行以下各项操作,并记住使用花括号来读取这些变量!
- 将
all替换为{props.name}。 - 将
aria-pressed的值设置为{props.isPressed}。 - 添加一个
onClick处理程序,该处理程序以过滤器的名称调用props.setFilter()。
完成所有这些后,您的 FilterButton.jsx 文件应该如下所示:
function FilterButton(props) {
return (
<button
type="button"
className="btn toggle-btn"
aria-pressed={props.isPressed}
onClick={() => props.setFilter(props.name)}>
<span className="visually-hidden">Show </span>
<span>{props.name}</span>
<span className="visually-hidden"> tasks</span>
</button>
);
}
export default FilterButton;
再次访问您的浏览器。您应该会看到不同的按钮被赋予了各自的名称。当您按下过滤按钮时,您应该会看到其文本呈现新的轮廓——这表明它已被选中。如果您在点击按钮时查看 DevTool 的页面检查器,您会看到 aria-pressed 属性值相应地发生变化。

然而,我们的按钮仍然没有真正过滤 UI 中的待办事项!让我们完成这项工作。
在 UI 中过滤任务
目前,App() 中的 taskList 常量遍历任务状态并为所有任务返回一个新的 <Todo /> 组件。这不是我们想要的!任务只有在包含在应用所选过滤器的结果中时才应该渲染。在我们遍历任务状态之前,我们应该对其进行过滤(使用 Array.prototype.filter())以消除我们不想渲染的对象。
像这样更新您的 taskList
const taskList = tasks
.filter(FILTER_MAP[filter])
.map((task) => (
<Todo
id={task.id}
name={task.name}
completed={task.completed}
key={task.id}
toggleTaskCompleted={toggleTaskCompleted}
deleteTask={deleteTask}
editTask={editTask}
/>
));
为了决定在 Array.prototype.filter() 中使用哪个回调函数,我们访问 FILTER_MAP 中与我们的过滤器状态键对应的值。例如,当过滤器为 All 时,FILTER_MAP[filter] 将评估为 () => true。
现在,在浏览器中选择一个过滤器将删除不符合其条件的任务。列表上方标题中的计数也将更改以反映列表!

总结
至此,我们的应用程序功能已经完整。然而,既然我们已经实现了所有功能,我们可以进行一些改进,以确保更广泛的用户可以使用我们的应用程序。我们的下一篇文章通过研究在 React 中包含焦点管理来结束我们的 React 教程,这可以提高可用性并减少键盘用户和屏幕阅读器用户的困惑。