Table of Contents

1、概述

接下来,我们要基于 useState 来实现一个非常复杂的案例,这个案例是一个待办事项的列表。

具体能支持的功能如下所示,学习之前可以先操作一下,支持添加、删除、编辑、统计待办事项数量等功能。

预览

虽然比较复杂,但是我们也不用过于担心,在前面我们学习了数据驱动 UI,因此只需要设计好数据,并针对数据进行操作即可。

2、设计数据

首先我们先定义好每一个待办事项的数据结构

index.tsx
1
interface TodoItem {
2
id: number
3
text: string
4
completed: boolean
5
}

然后在数据设计上,我们首先得有一个列表,那么我们可以使用 useState 来定义一个数组,数组中存储的是待办事项。

index.tsx
1
// 定义待办事项列表并初始化数据
2
const [todos, setTodos] = useState<TodoItem[]>([
3
{ id: 1, text: '学习 React useState', completed: true },
4
{ id: 2, text: '完成项目文档', completed: false },
5
{ id: 3, text: '准备技术分享', completed: false }
6
])

那么,如果我们想要新增一条数据,只需要在原有数据的基础之上,添加一条数据即可。

index.tsx
1
const addTodo = () => {
2
// 如果新输入的待办事项不为空,则添加到列表中
3
if (newTodo.trim()) {
4
// 获取当前最大的 id,并加 1
5
const newId = Math.max(...todos.map(t => t.id), 0) + 1
6
// 添加到列表中
7
setTodos([...todos, { id: newId, text: newTodo.trim(), completed: false }])
8
// 清空新增待办事项的输入框
9
setNewTodo('')
10
}
11
}

这里需要注意,新数组传入 setTodos 函数中,必须要引用发生改变,才会触发组件的重新渲染。因此下面的写法是错误的

index.tsx
1
todos.push({ id: newId, text: newTodo.trim(), completed: false })
2
// 由于引用没改变,React 的浅比较会认为数据也没变
3
setTodos(todos)

同样的道理,删除数据,只需要在原有数据的基础之上,过滤掉目标 id 的项即可。

index.tsx
1
const deleteTodo = (id: number) => {
2
setTodos(todos.filter(t => t.id !== id))
3
}

编辑数据,只需要在原有数据的基础之上,找到目标 id 的项,并更新其内容即可。

index.tsx
1
// 切换每一项的完成状态
2
const toggleComplete = (id: number) => {
3
setTodos(todos.map(todo =>
4
todo.id === id ? { ...todo, completed: !todo.completed } : todo
5
))
6
}
index.tsx
1
// 修改某一项的内容
2
setTodos(todos.map(todo =>
3
todo.id === id ? { ...todo, text: editingText.trim() } : todo
4
))
INFO

filter map 等方法的执行结果,都会返回一个新的数组,因此引用会发生变化

在这个案例中,还包含其他两个状态,一个是新增待办事项的输入框内容,一个是当前正在编辑的待办事项,我们分别定义他们即可

code.ts
1
// 新增待办事项的输入框内容
2
const [newTodo, setNewTodo] = useState('')
3
// 当前正在编辑的待办事项
4
const [editingId, setEditingId] = useState<number | null>(null)
5
// 当前正在编辑的待办事项的内容
6
const [editingText, setEditingText] = useState('')

结合 props 与子组件封装,以及付子组件通信的知识,完整的代码如下所示

index.tsx
TodoHeader.tsx
TodoList.tsx
TodoItem.tsx
TodoInput.tsx
TodoStats.tsx
1
import { useState } from 'react'
2
import TodoHeader from './TodoHeader'
3
import TodoInput from './TodoInput'
4
import TodoList from './TodoList'
5
import TodoStats from './TodoStats'
6
7
interface TodoItem {
8
id: number
9
text: string
10
completed: boolean
11
}
12
13
export default function ArrayState() {
14
const [todos, setTodos] = useState<TodoItem[]>([
15
{ id: 1, text: '学习 React useState', completed: true },
16
{ id: 2, text: '完成项目文档', completed: false },
17
{ id: 3, text: '准备技术分享', completed: false }
18
])
19
20
const [newTodo, setNewTodo] = useState('')
21
const [editingId, setEditingId] = useState<number | null>(null)
22
const [editingText, setEditingText] = useState('')
23
24
const addTodo = () => {
25
if (newTodo.trim()) {
26
const newId = Math.max(...todos.map(t => t.id), 0) + 1
27
setTodos([...todos, { id: newId, text: newTodo.trim(), completed: false }])
28
setNewTodo('')
29
}
30
}
31
32
const deleteTodo = (id: number) => {
33
setTodos(todos.filter(todo => todo.id !== id))
34
}
35
36
const toggleComplete = (id: number) => {
37
setTodos(todos.map(todo =>
38
todo.id === id ? { ...todo, completed: !todo.completed } : todo
39
))
40
}
41
42
const startEditing = (id: number, text: string) => {
43
setEditingId(id)
44
setEditingText(text)
45
}
46
47
const saveEdit = (id: number) => {
48
if (editingText.trim()) {
49
setTodos(todos.map(todo =>
50
todo.id === id ? { ...todo, text: editingText.trim() } : todo
51
))
52
}
53
setEditingId(null)
54
setEditingText('')
55
}
56
57
const cancelEdit = () => {
58
setEditingId(null)
59
setEditingText('')
60
}
61
62
const handleKeyPress = (e: React.KeyboardEvent) => {
63
if (e.key === 'Enter') {
64
addTodo()
65
}
66
}
67
68
const completedCount = todos.filter(todo => todo.completed).length
69
const totalCount = todos.length
70
71
return (
72
<div className="flex flex-col gap-4 p-4">
73
<TodoHeader
74
completedCount={completedCount}
75
totalCount={totalCount}
76
/>
77
78
<TodoInput
79
value={newTodo}
80
onChange={setNewTodo}
81
onAdd={addTodo}
82
onKeyPress={handleKeyPress}
83
/>
84
85
<TodoList
86
todos={todos}
87
editingId={editingId}
88
editingText={editingText}
89
onToggleComplete={toggleComplete}
90
onDelete={deleteTodo}
91
onStartEdit={startEditing}
92
onSaveEdit={saveEdit}
93
onCancelEdit={cancelEdit}
94
onEditingTextChange={setEditingText}
95
/>
96
97
<TodoStats
98
totalCount={totalCount}
99
completedCount={completedCount}
100
/>
101
</div>
102
)
103
}

3、总结

本案例相对比较复杂,但是我们只要理清了数据变化的逻辑,那么实现起来就非常简单了。在学习本案例时,大家需要多花一点时间揣摩代码,并理解数据驱动 UI 的思维真意。

专栏首页
到顶
专栏目录