接下来,我们要基于 useState 来实现一个非常复杂的案例,这个案例是一个待办事项的列表。
具体能支持的功能如下所示,学习之前可以先操作一下,支持添加、删除、编辑、统计待办事项数量等功能。
虽然比较复杂,但是我们也不用过于担心,在前面我们学习了数据驱动 UI,因此只需要设计好数据,并针对数据进行操作即可。
首先我们先定义好每一个待办事项的数据结构
1interface TodoItem {2id: number3text: string4completed: boolean5}
然后在数据设计上,我们首先得有一个列表,那么我们可以使用 useState 来定义一个数组,数组中存储的是待办事项。
1// 定义待办事项列表并初始化数据2const [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])
那么,如果我们想要新增一条数据,只需要在原有数据的基础之上,添加一条数据即可。
10const addTodo = () => {20// 如果新输入的待办事项不为空,则添加到列表中30if (newTodo.trim()) {40// 获取当前最大的 id,并加 150const newId = Math.max(...todos.map(t => t.id), 0) + 160// 添加到列表中70setTodos([...todos, { id: newId, text: newTodo.trim(), completed: false }])80// 清空新增待办事项的输入框90setNewTodo('')10}11}
这里需要注意,新数组传入 setTodos
函数中,必须要引用发生改变,才会触发组件的重新渲染。因此下面的写法是错误的
1todos.push({ id: newId, text: newTodo.trim(), completed: false })2// 由于引用没改变,React 的浅比较会认为数据也没变3setTodos(todos)
同样的道理,删除数据,只需要在原有数据的基础之上,过滤掉目标 id 的项即可。
1const deleteTodo = (id: number) => {2setTodos(todos.filter(t => t.id !== id))3}
编辑数据,只需要在原有数据的基础之上,找到目标 id 的项,并更新其内容即可。
1// 切换每一项的完成状态2const toggleComplete = (id: number) => {3setTodos(todos.map(todo =>4todo.id === id ? { ...todo, completed: !todo.completed } : todo5))6}
1// 修改某一项的内容2setTodos(todos.map(todo =>3todo.id === id ? { ...todo, text: editingText.trim() } : todo4))
filter map 等方法的执行结果,都会返回一个新的数组,因此引用会发生变化
在这个案例中,还包含其他两个状态,一个是新增待办事项的输入框内容,一个是当前正在编辑的待办事项,我们分别定义他们即可
1// 新增待办事项的输入框内容2const [newTodo, setNewTodo] = useState('')3// 当前正在编辑的待办事项4const [editingId, setEditingId] = useState<number | null>(null)5// 当前正在编辑的待办事项的内容6const [editingText, setEditingText] = useState('')
结合 props 与子组件封装,以及付子组件通信的知识,完整的代码如下所示
100import { useState } from 'react'200import TodoHeader from './TodoHeader'300import TodoInput from './TodoInput'400import TodoList from './TodoList'500import TodoStats from './TodoStats'600700interface TodoItem {800id: number900text: string100completed: boolean110}120130export default function ArrayState() {140const [todos, setTodos] = useState<TodoItem[]>([150{ id: 1, text: '学习 React useState', completed: true },160{ id: 2, text: '完成项目文档', completed: false },170{ id: 3, text: '准备技术分享', completed: false }180])190200const [newTodo, setNewTodo] = useState('')210const [editingId, setEditingId] = useState<number | null>(null)220const [editingText, setEditingText] = useState('')230240const addTodo = () => {250if (newTodo.trim()) {260const newId = Math.max(...todos.map(t => t.id), 0) + 1270setTodos([...todos, { id: newId, text: newTodo.trim(), completed: false }])280setNewTodo('')290}300}310320const deleteTodo = (id: number) => {330setTodos(todos.filter(todo => todo.id !== id))340}350360const toggleComplete = (id: number) => {370setTodos(todos.map(todo =>380todo.id === id ? { ...todo, completed: !todo.completed } : todo390))400}410420const startEditing = (id: number, text: string) => {430setEditingId(id)440setEditingText(text)450}460470const saveEdit = (id: number) => {480if (editingText.trim()) {490setTodos(todos.map(todo =>500todo.id === id ? { ...todo, text: editingText.trim() } : todo510))520}530setEditingId(null)540setEditingText('')550}560570const cancelEdit = () => {580setEditingId(null)590setEditingText('')600}610620const handleKeyPress = (e: React.KeyboardEvent) => {630if (e.key === 'Enter') {640addTodo()650}660}670680const completedCount = todos.filter(todo => todo.completed).length690const totalCount = todos.length700710return (720<div className="flex flex-col gap-4 p-4">730<TodoHeader740completedCount={completedCount}750totalCount={totalCount}760/>770780<TodoInput790value={newTodo}800onChange={setNewTodo}810onAdd={addTodo}820onKeyPress={handleKeyPress}830/>840850<TodoList860todos={todos}870editingId={editingId}880editingText={editingText}890onToggleComplete={toggleComplete}900onDelete={deleteTodo}910onStartEdit={startEditing}920onSaveEdit={saveEdit}930onCancelEdit={cancelEdit}940onEditingTextChange={setEditingText}950/>960970<TodoStats980totalCount={totalCount}990completedCount={completedCount}100/>101</div>102)103}
本案例相对比较复杂,但是我们只要理清了数据变化的逻辑,那么实现起来就非常简单了。在学习本案例时,大家需要多花一点时间揣摩代码,并理解数据驱动 UI 的思维真意。