搭建思路:实现一个简单的待办事项(Todos)应用,支持添加、切换完成状态、删除待办,并采用现代 React(函数组件 + 自定义 Hook)实现,结构清晰、易维护。
一、开发前的准备工作:项目初始化和样式设计
1.使用 Vite 初始化 React 项目
bash
npm create vite@latest . -- --template react
npm install
选择 react 模板,Vite 会自动生成基础目录和配置。
bash
npm run dev
浏览器访问本地地址,看到 React 欢迎页即初始化成功。
2.清理模板代码
- 删除 App.css、App.jsx、main.jsx 等文件中的多余内容,只保留最基础的结构。
- 删除或重命名 Vite 自带的 logo、svg 等资源。
3.搭建现代化的 CSS 环境:Stylus
- 为什么用 Stylus?
Stylus 是一种 CSS 预处理器,支持变量、嵌套、函数等高级特性,让样式编写更高效、结构更清晰。 - 如何集成?
- 安装依赖:
npm install stylus stylus-loader --save-dev
- 在
src/global.styl
编写全局样式,体验变量、嵌套等特性。 - 在组件中
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 实现响应式
jsxconst [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.json
或vite.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
负责状态管理和方法分发,子组件如TodoForm
、TodoList
、TodoItem
各司其职,专注于界面展示和用户交互。