React Hooks 实战:从一个 Todo List 看自定义 Hooks 的优雅与坑
大家好,自从 React 16.8 引入 Hooks 以来,它彻底改变了我们写组件的方式。过去类组件的生命周期散乱、状态逻辑复用难的问题,现在用 Hooks 可以轻松解决。Hooks 不只是“函数式状态管理”,它是一种函数式编程思想,让代码更模块化、更易维护、更可复用。
一、为什么选择 Hooks?类组件的痛点与 Hooks 的救赎
先回忆一下类组件的痛点:
- 生命周期分散:同一个功能(如数据获取)可能散落在 componentDidMount、componentDidUpdate 中,代码阅读性差。
- 状态逻辑复用难:HOC(高阶组件)或 Render Props 会导致“包裹地狱”,组件树层级爆炸。
- this 绑定麻烦:箭头函数、bind() 到处飞,容易出错。
Hooks 的出现,像一股清风:
- 函数式组件更纯粹:UI 就是 HTML + CSS,业务逻辑抽到 Hooks 中。
- 逻辑复用无痛:自定义 Hooks 让状态逻辑像普通函数一样复用,不增加组件层级。
- 代码更聚合:相关逻辑集中在一个 Hook 中,可读性爆棚。
在我们的 Todo List 项目中,就完美体现了这一点:todos 的增删改查全封装在 useTodos Hook 中,鼠标位置实时跟踪封装在 useMouse 中。组件只负责渲染,干净利落!
二、核心案例一:useTodos —— 持久化 Todo 列表的完美封装
来看一个自定义 Hook:useTodos.js
import { useState, useEffect } from "react";
const STORAGE_KEY = 'todos';
function loadFromStorage() {
const storedTodos = localStorage.getItem(STORAGE_KEY);
return storedTodos ? JSON.parse(storedTodos) : [];
}
function saveToStorage(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}
export const useTodos = () => {
const [todos, setTodos] = useState(loadFromStorage());
useEffect(() => {
saveToStorage(todos);
}, [todos]);
const addTodo = (text) => {
setTodos([
...todos,
{
id: Date.now(),
text,
completed: false
}
]);
};
const toggleTodo = (id) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return { todos, addTodo, toggleTodo, deleteTodo };
};
底层逻辑拆解
-
初始状态从 localStorage 加载:
useState(loadFromStorage()):这里传函数是懒加载,只有第一次渲染时执行。完美避免每次渲染都读 storage 的性能浪费。
-
状态变化时持久化:
useEffect(() => saveToStorage(todos), [todos]):依赖 todos,当 todos 变化时自动保存。经典的“副作用”处理。
-
操作函数返回:
- addTodo、toggleTodo、deleteTodo 都是纯函数式更新状态的方式,使用不可变数据(...spread、map、filter),符合 React 最佳实践。
使用方式超级简洁
在 App.jsx 中:
const { todos, addTodo, deleteTodo, toggleTodo } = useTodos();
然后传给子组件:
<TodoInput addTodo={addTodo} />
<TodoList todos={todos} onDelete={deleteTodo} onToggle={toggleTodo} />
组件完全解耦!如果以后想换成 Redux 或 Context,只改 Hook 内部就行。
三、核心案例二:useMouse —— 事件监听与内存泄漏的生死较量
另一个经典 Hook:useMouse.js
export const useMouse = () => {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
useEffect(() => {
const update = (event) => {
setX(event.pageX);
setY(event.pageY);
};
window.addEventListener('mousemove', update);
return () => {
window.removeEventListener('mousemove', update);
};
}, []); // 空依赖,只挂载一次
return { x, y };
};
底层逻辑:useEffect 的清理函数
- useEffect 的返回值是一个清理函数,在组件卸载或下次 effect 执行前调用。
- 这里监听全局 mousemove,不清理会导致:组件卸载后事件仍绑定,状态更新试图 setState 到已卸载组件 → 内存泄漏警告。
易错点大提醒:
- 忘记 return 清理函数 → 内存泄漏!
- 依赖数组错写 → 重复绑定或不更新。
类似场景:setInterval、WebSocket、订阅事件,都必须清理。
“组件卸载时,需要清除事件监听/定时器,否则会导致内存泄漏”
四、组件层的设计:纯 UI + Props 驱动
看看 TodoItem、TodoList、TodoInput:
- 全是纯函数组件,只接收 props,零状态。
- TodoItem:checkbox + span(带 completed 类) + 删除按钮。
- TodoInput:受控输入 + submit 添加。
- TodoList:map todos 生成 TodoItem。
这种“聪明 Hook + 哑组件”模式,是 Hooks 时代的最佳实践。组件只管渲染,逻辑全在 Hook。
五、自定义 Hooks 最佳实践总结
- 命名必须 use 开头:让 ESLint 插件识别。
- 只在顶层调用 Hook:别放循环、条件里。
- 逻辑分离:一个 Hook 专注一件事(如 useTodos 只管 todos)。
- 依赖数组写全:用 eslint-plugin-react-hooks 的 exhaustive-deps 规则自动检查。
- 尽量少用 useEffect:能用纯状态逻辑就别用 effect。
- 复杂状态用 useReducer:todos 操作多时,可改用 reducer 更清晰。
- 复用优先:相同逻辑多个组件用?抽 Hook!
六、结语:Hooks 是前端团队的核心资产
通过这个 Todo List 项目,我们看到 Hooks 如何让代码从“乱麻”变成“丝滑”。自定义 Hooks 就像乐高积木,组合出无限可能。它不只是语法糖,而是提升代码质量、团队协作的利器。
下次写组件时,问自己:这个逻辑能不能抽成 Hook?多练几次,你会爱上这种函数式优雅!