用 react + ts 实现我的第一个 todoList
跟着吴悠老师做了这个todoList小练手,下面是关于这个小练手的一些总结

一、项目结构
这个项目拆成了几个小文件,分别负责不同的功能:
page.tsx
:页面入口,管理任务数据(todos)和筛选条件(filter)types.ts
:定义任务(Todo)的数据类型AddTodo.tsx
:新增任务输入框TodoList.tsx
:渲染任务列表TodoItem.tsx
:单个任务的展示和操作(切换/删除)TodoFilter.tsx
:筛选任务(全部 / 已完成 / 未完成)
React 比较推荐"组件拆分",每个功能单独写一个小文件。
二、核心数据结构
在 types.ts
先定义了一个任务对象的类型:
typescript
export interface Todo {
id: number;
text: string;
completed: boolean;
}
这样在写 React 组件时,TypeScript 就能帮助检查:
id
必须是数字text
必须是字符串completed
必须是布尔值
有 Vue 和 JS 基础的朋友应该知道,JS 里类型随便改不会报错,而 TS 能在写代码时就发现问题。
三、页面入口(page.tsx)
在 page.tsx
里,使用 React 的 useState
来存放任务数据和筛选状态:
css
const [todos, setTodos] = useState<Todo[]>([])
const [filter, setFilter] = useState<string>("all")
这里 useState
的用法类似 Vue 的 ref
,它会返回一个状态值和一个更新函数。 比如 todos
就是任务数组,更新它必须用 setTodos
。
新增任务
新增时,把输入的内容塞进一个新对象,然后放进数组:
vbnet
const addTodo = (text: string) => {
const newTodo = {
id: Date.now(),
text,
completed: false,
}
setTodos([...todos, newTodo])
}
这里用 Date.now()
生成一个 id,就能保证唯一性。
删除任务
我这里的"删除"并不是直接移除,而是把任务标记为完成:
typescript
const deleteTodo = (id: number) => {
setTodos(todos.map(todo => {
if (todo.id === id) {
return { ...todo, completed: true }
}
return todo
}))
}
切换完成状态
用 map
遍历数组,找到目标任务,然后把 completed
取反:
ini
const toggleTodo = (id: number) => {
setTodos(todos.map(todo => {
if (todo.id === id) {
return { ...todo, completed: !todo.completed }
}
return todo
}))
}
四、子组件拆分
1. AddTodo(新增任务)
表单提交时调用 addTodo
,然后清空输入框:
ini
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Add Todo"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">新建事项</button>
</form>
和 Vue 不同的是,React 这里没有 v-model
,而是"受控组件": value
来自状态,onChange
改变状态。
2. TodoList + TodoItem(任务展示)
列表组件把 todos
遍历渲染成一个个 TodoItem
:
ini
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
/>
))}
</ul>
每个任务项里用简单的 style
来控制样式:
css
<li style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
{todo.text}
<button onClick={() => toggleTodo(todo.id)}>切换</button>
<button onClick={() => deleteTodo(todo.id)}>删除</button>
</li>
和 Vue 的 :class="{done: todo.completed}"
差不多,不过 React 常用内联样式或 className。
3. TodoFilter(筛选器)
三个按钮,分别设置过滤条件:
less
<button onClick={() => setFilter("all")}>All</button>
<button onClick={() => setFilter("active")}>Active</button>
<button onClick={() => setFilter("completed")}>Completed</button>
这里的 setFilter
就是从父组件传下来的函数。
五、任务筛选逻辑
最后就是根据 filter
返回不同的任务:
dart
const getFilteredTodos = () => {
switch (filter) {
case "completed":
return todos.filter(todo => todo.completed)
case "active":
return todos.filter(todo => !todo.completed)
default:
return todos
}
}
在渲染时,把 getFilteredTodos()
的结果交给 TodoList
。
六、完整代码
types.ts
typescript
export interface Todo{
id:number;
text:string;
completed:boolean;
}
page.tsx
typescript
"use client"
import AddTodo from "../app/components/AddTodo"
import TodoList from "../app/components/TodoList"
import TodoFilter from "./components/TodoFilter";
import { useState } from "react";
import { Todo } from "./types";
export default function Home() {
const [todos,setTodos] = useState<Todo[]>([])
const [filter, setFilter] = useState<string>("all")
const addTodo = (text: string) => {
const newTodo = {
id: Date.now(),
text,
completed: false,
}
setTodos([...todos, newTodo])
}
const deleteTodo = (id: number) => {
setTodos(todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed: true
}
}
return todo
}))
}
const toggleTodo = (id: number) => {
setTodos(todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed: !todo.completed
}
}
return todo
}))
}
const getFilteredTodos = () => {
switch (filter) {
case "all":
return todos
case "completed":
return todos.filter(todo => todo.completed)
case "active":
return todos.filter(todo => !todo.completed)
default:
return todos
}
}
return (
<div>
<h1>Todo List</h1>
<AddTodo addTodo={addTodo} />
<TodoList todos={getFilteredTodos()} deleteTodo={deleteTodo} toggleTodo={toggleTodo}/>
<TodoFilter setFilter={setFilter}/>
</div>
);
}
AddTodo.tsx
typescript
import {useState} from "react";
interface AddTodoProps{
addTodo: (text: string) => void
}
function AddTodo({addTodo}: AddTodoProps) { // TypeScript 的类型注解
const [text, setText] = useState<string>("")
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if(text.trim() === ""){
alert("Please add a todo")
return
}
addTodo(text)
setText("")
}
return(
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Add Todo"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">新建事项</button>
</form>
)
}
export default AddTodo
TodoFilter.tsx
javascript
type TodoFilterProps = {
setFilter: (filter: "all" | "active" | "completed") => void;
};
function TodoFilter({setFilter}:TodoFilterProps){
return (
<div>
<button onClick={() => setFilter("all")}>All</button>
<button onClick={() => setFilter("active")}>Active</button>
<button onClick={() => setFilter("completed")}>Completed</button>
</div>
)
}
export default TodoFilter
Todoltem.tsx
javascript
function TodoItem({todo,toggleTodo,deleteTodo}: any) {
return (
<li style={{
textDecoration: todo.completed ? "line-through" : "none"
}}>
{todo.text}
<button onClick={() => toggleTodo(todo.id)}>切换</button>
<button onClick={() => deleteTodo(todo.id)}>删除</button>
</li>
)
}
export default TodoItem
TodoList.tsx
typescript
import {Todo} from "../types"
import TodoItem from "./TodoItem";
interface TodoListProps {
todos: Array<Todo>;
toggleTodo: (id: number) => void;
deleteTodo: (id: number) => void;
}
function TodoList({todos,toggleTodo,deleteTodo}:TodoListProps) {
return(
<ul>
{todos.map(todo =>(
<TodoItem key={todo.id} todo={todo} toggleTodo={toggleTodo} deleteTodo={deleteTodo}></TodoItem>
))}
</ul>
)
}
export default TodoList
六、总结
- 状态管理 :用
useState
保存任务和筛选条件,类似 Vue 的ref
。 - 组件通信 :通过 props 传函数(比如
addTodo
、setFilter
),相当于 Vue 的props + emit
。 - 受控表单 :输入框的
value
和onChange
绑定状态,没有 Vue 的v-model
,但逻辑很清晰。 - 类型检查 :定义
Todo
接口,让代码更规范。