React 自定义 Hook Todo 应用全流程实战

搭建思路:实现一个简单的待办事项(Todos)应用,支持添加、切换完成状态、删除待办,并采用现代 React(函数组件 + 自定义 Hook)实现,结构清晰、易维护。

一、开发前的准备工作:项目初始化和样式设计

1.使用 Vite 初始化 React 项目

bash 复制代码
npm create vite@latest . -- --template react
npm install

选择 react 模板,Vite 会自动生成基础目录和配置。

bash 复制代码
npm run dev

浏览器访问本地地址,看到 React 欢迎页即初始化成功。

2.清理模板代码

  1. 删除 App.css、App.jsx、main.jsx 等文件中的多余内容,只保留最基础的结构。
  2. 删除或重命名 Vite 自带的 logo、svg 等资源。

3.搭建现代化的 CSS 环境:Stylus

  • 为什么用 Stylus?
    Stylus 是一种 CSS 预处理器,支持变量、嵌套、函数等高级特性,让样式编写更高效、结构更清晰。
  • 如何集成?
    1. 安装依赖:npm install stylus stylus-loader --save-dev
    2. src/global.styl 编写全局样式,体验变量、嵌套等特性。
    3. 在组件中 import '../global.styl' 即可生效。

(二)组件架构

css 复制代码
src/
├── App.jsx:根组件
├── components/:存放 UI 组件
│   └── todos/:待办相关组件
│       ├── index.jsx:聚合组件
│       ├── TodoForm.jsx:新增表单
│       ├── TodoList.jsx:列表
│       └── TodoItem.jsx:单项
├── hooks/:自定义 Hook
│   └── useTodos.js
├── index.jsx、main.jsx:入口文件
└── 样式文件

顶层组件 App

  • 作为应用的根组件,负责整体布局和页面结构。

2. 业务主组件 Todos

  • 负责待办事项的核心功能和页面展示。
  • 在内部调用自定义 Hook(useTodos)来管理 todos 的数据和操作方法。
  • 只关注 UI 结构和 props 传递,不处理具体业务逻辑。

3. 子组件

1)TodoForm

  • 新增待办事项的表单组件。
  • 包含输入框和添加按钮,用户输入内容后调用 addTodo 方法。

2)TodoList

  • 展示所有待办事项的列表组件。
  • 接收 todos 数据,遍历渲染每一项。

3)TodoItem

  • 单个待办事项的展示组件。
  • 显示 todo 内容,支持切换完成状态和删除操作。

二、React相关实现原理

(一)组件通信

  • props 单向传递

    jsx 复制代码
    // 父组件
    <TodoForm addTodo={addTodo} />
    // 子组件
    props.addTodo('新任务')
  • 父组件定义一个函数(比如 addTodo),通过 props 传递给子组件(TodoForm)。

  • 子组件拿到 props.addTodo 后,调用它并传参(比如 '新任务'),实现 "子组件通知父组件执行逻辑"。

(二)数据绑定

  • useState 实现响应式

    jsx 复制代码
    const [todos, setTodos] = useState([]);
  • todos 是当前状态(初始值是空数组 []),存所有待办项。

  • setTodos 是修改状态的函数,调用它会触发组件重新渲染。

  • 数据驱动视图

    jsx 复制代码
    {todos.map(todo => <TodoItem todo={todo} />)}
  • 遍历 todos 数组(状态数据),为每个 todo 项生成一个 TodoItem 组件。

  • 通过 props.todo 把单个待办数据传给子组件(TodoItem),让子组件渲染具体内容。

(三)本地存储

js 复制代码
useEffect(() => {
  localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
  • 利用 useEffect 钩子,在 todos 状态发生变化时,把最新的待办事项列表 todos 存储到浏览器本地存储(localStorage)中。
js 复制代码
const [todos, setTodos] = useState(() => {
  const saved = localStorage.getItem('todos');
  return saved ? JSON.parse(saved) : [];
});
  • 组件初始化时,从 localStorage 中读取之前存储的待办事项数据,作为 todos 状态的初始值 。
  • return saved ? JSON.parse(saved) : [] :如果获取到了数据(saved 存在),就用 JSON.parse 把 JSON 字符串转成数组(还原成 JavaScript 能识别的对象 / 数组格式);要是没获取到(比如第一次使用,本地还没存),就返回空数组 [] ,作为 todos 的初始值 。

三、自定义 Hook 实战

(一)什么是自定义 Hook?

在 React 中,自定义 Hook(Custom Hook)是以 use 开头的函数,用来复用组件之间的状态逻辑。

它本质上就是一个普通的 JS 函数,但可以使用 React 的内置 Hook(如 useState、useEffect 等)。

(二)useTodos Hook: 集中管理"待办事项"的所有业务逻辑和状态

js 复制代码
import { useState } from "react"; // 引入 React 的 useState,用于声明状态变量

export default function useTodos() { // 定义自定义 Hook,名称以 use 开头,方便复用逻辑
  const [todos, setTodos] = useState([]); // 声明 todos 状态,初始值为空数组,setTodos 用于更新 todos

  // 新增 todo 的方法,接收文本参数 text
  const addTodo = (text) => {
    setTodos([
      ...todos, // 保留原有 todos
      {
        id: Date.now(), // 用当前时间戳生成唯一 id
        text, // 用户输入的内容
        completed: false, // 新增的 todo 默认未完成
      },
    ]);
  };

  // 切换 todo 完成状态的方法,接收要切换的 todo 的 id
  const toggleTodo = (id) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id // 找到 id 匹配的 todo
          ? { ...todo, completed: !todo.completed } // 切换 completed 字段
          : todo // 其他 todo 保持不变
      )
    );
  };

  // 删除 todo 的方法,接收要删除的 todo 的 id
  const removeTodo = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id)); // 过滤掉 id 匹配的 todo,保留其他 todos
  };

  // 返回 todos 状态和三个操作方法,供组件使用
  return {
    todos, // 当前所有待办事项
    addTodo, // 新增 todo 的方法
    toggleTodo, // 切换完成状态的方法
    removeTodo, // 删除 todo 的方法
  };
}

(三)Todos 组件: 不关心数据怎么处理,只用 Hook 提供的能力

jsx 复制代码
import useTodos from '../hooks/useTodos'; // 引入自定义 Hook,用于管理 todos 的数据和逻辑
import TodoForm from './TodoForm.jsx'; // 引入新增待办事项的表单组件
import TodoList from './TodoList.jsx'; // 引入展示待办事项列表的组件

export default function Todos() { // 定义 Todos 组件,作为 todos 功能的主界面
  // 调用 useTodos 自定义 Hook,获取 todos 数据和操作方法
  // 这样 UI 组件只关注界面,所有业务逻辑都交给 Hook
  const { todos, addTodo, removeTodo, toggleTodo } = useTodos();

  // 渲染 UI:包含新增表单和待办列表
  // addTodo 传给 TodoForm,负责添加新待办
  // todos、toggleTodo、removeTodo 传给 TodoList,负责展示、切换和删除
  return (
    <div>
      <TodoForm addTodo={addTodo} /> {/* 表单组件,用户输入后调用 addTodo 新增待办 */}
      <TodoList todos={todos} toggleTodo={toggleTodo} removeTodo={removeTodo} /> {/* 列表组件,展示所有 todos,并支持切换和删除 */}
    </div>
  );
}

四、进阶优化

(一)路径别名:告别 "../../" 地狱

  • 配置路径别名
    jsconfig.jsonvite.config.js 里配置 @ 指向 src,引入更清晰。

    json 复制代码
    {
      "compilerOptions": {
        "baseUrl": "src"
      }
    }
    js 复制代码
    // 现在可以这样写
    import useTodos from '@/hooks/useTodos';

(二)跨组件通信优化:用 useContext 解决 "层级太深"

  • Context 方案
    当多个层级需要共享 todos 或操作时,用 Context 提供全局状态,避免 props 层层传递。

    js 复制代码
    // 创建 Context
    export const TodoContext = React.createContext();
    // Provider 包裹
    <TodoContext.Provider value={...}>{children}</TodoContext.Provider>
    // 子组件用 useContext(TodoContext) 获取

(三)性能细节

1. 组件拆分与最小化重渲染

组件拆分

项目将 UI 拆分为 TodoForm、TodoList、TodoItem 等小组件,每个组件只负责自己的功能。这样可以减少每次状态变化时的重渲染范围,提高渲染效率。

只传递必要的 props

父组件 Todos 只把必要的数据和方法传递给子组件,避免无关数据导致子组件无谓重渲染。

2. 自定义 Hook 的性能优势

  • useTodos 自定义 Hook 将所有业务逻辑集中管理,组件只负责 UI。
  • 这样可以让逻辑复用,减少重复代码,也让状态变更只影响真正需要的组件。

3. React 内置优化机制

useState 的局部性

  • useState 只会引起当前组件的重渲染,不会影响无关组件。

  • 只有当 todos、addTodo、removeTodo、toggleTodo 这些 props 发生变化时,TodoList、TodoForm 等子组件才会重渲染。

小结

该项目通过自定义 Hook(useTodos)将待办事项的增删改查等业务逻辑与 UI 组件彻底分离,实现了高内聚、低耦合的代码结构。组件架构层次分明,主功能组件 Todos 负责状态管理和方法分发,子组件如TodoFormTodoListTodoItem 各司其职,专注于界面展示和用户交互。

相关推荐
安心不心安6 分钟前
React hooks——useReducer
前端·javascript·react.js
啃火龙果的兔子8 分钟前
react19+nextjs+antd切换主题颜色
前端·javascript·react.js
谢尔登3 小时前
【React Native】布局和 Stack 、Slot
javascript·react native·react.js
几颗流星4 小时前
01 react入门
前端·react.js
遂心_5 小时前
React Fragment与DocumentFragment:提升性能的双剑合璧
前端·javascript·react.js
混水的鱼5 小时前
PasswordValidation 密码校验组件实现与详解
前端·react.js
自己记录_理解更深刻5 小时前
默认导出 vs 具名导出
react.js
WildBlue6 小时前
小白也能懂!react-router-dom 超详细指南🚀
前端·react.js
孟陬6 小时前
tailwind“移动端优先”在隐藏元素方面的问题 - tailwindcss 系列
react.js
颜酱6 小时前
使用useReducer和Context进行React中的页面内部数据共享
前端·javascript·react.js