用 react + ts 实现我的第一个 todoList

用 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

六、总结

  1. 状态管理 :用 useState 保存任务和筛选条件,类似 Vue 的 ref
  2. 组件通信 :通过 props 传函数(比如 addTodosetFilter),相当于 Vue 的 props + emit
  3. 受控表单 :输入框的 valueonChange 绑定状态,没有 Vue 的 v-model,但逻辑很清晰。
  4. 类型检查 :定义 Todo 接口,让代码更规范。
相关推荐
然我21 分钟前
React 16.8:不止 Hooks 那么简单,这才是真正的划时代更新 🚀
前端·react.js·前端框架
OEC小胖胖38 分钟前
【React Hooks】封装的艺术:如何编写高质量的 React 自-定义 Hooks
前端·react.js·前端框架·web
木春3 小时前
React入门:构建你的第一个应用
前端·react.js
吃奥特曼的饼干6 小时前
React useEffect 清理函数:别让依赖数组坑了你!
前端·react.js
随笔记7 小时前
react中函数式组件和类组件有什么区别?新建的react项目用函数式组件还是类组件?
前端·react.js·typescript
emojiwoo7 小时前
React 状态管理:useState 与 useDatePersistentState 深度对比
前端·javascript·react.js
D11_7 小时前
【React】JSX基础
前端·react.js·前端框架
晴空雨7 小时前
Zustand vs Redux Toolkit:现代 React 状态管理深度对比
前端·react.js
梨子同志7 小时前
React 组件
react.js