“React Hooks 不是魔法,但用好了,你就是魔法师。”
大家好!今天不聊八股文,不背面试题,咱们来点实战又有趣的——用 React Hooks 写一个能实时追踪鼠标位置的小功能,再顺手搭个清爽的待办事项(TodoList)应用。
更重要的是:我们还要自己封装两个自定义 Hook,让代码像乐高一样可复用、可组合、还贼优雅!
准备好了吗?Let’s Hook it up! 🎣
一、Hooks 是什么?为什么它让我爱上函数组件?
在 React 的远古时代(其实也就几年前),想管理状态和副作用?不好意思,请用 class 组件,写一堆 this.setState 和生命周期方法。
直到 React 16.8 带着 Hooks 闪亮登场——
函数组件也能拥有状态、副作用、甚至“超能力”!
useState:给函数组件加状态useEffect:处理副作用(比如发请求、监听事件)useContext:跨组件传数据- ……以及,你可以自己造轮子:
useYourOwnMagic()
而所有内置或自定义的 Hook 都有个不成文的规矩:名字必须以 use 开头。
这不只是约定,更是 React 的“咒语识别机制”——看到 useXxx,就知道:“哦,这是个 Hook,我要特殊对待。”
二、实战第一弹:用 useMouse 实时追踪鼠标位置 🖱️
想象一下:用户移动鼠标,页面上立刻显示坐标 (x, y)。听起来简单?但别忘了——内存泄漏的陷阱就在脚下!
❌ 错误示范(新手常踩坑):
// 千万别这么写!
useEffect(() => {
const handleMove = (e) => {
setX(e.clientX);
setY(e.clientY);
};
window.addEventListener('mousemove', handleMove);
// 忘了 removeEventListener?组件卸载后监听器还在跑!
}, []);
当组件被销毁(比如切换页面),这个监听器不会自动消失。它会继续默默运行,占用内存,甚至可能操作已卸载组件的状态——React 会警告你:“Can't perform a React state update on an unmounted component.”
✅ 正确姿势:用 useEffect 的返回函数清理副作用!
// hooks/useMouse.js
import { useState, useEffect } from 'react';
export function useMouse() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
// 👇 关键!返回一个清理函数
return () => {
window.removeEventListener('mousemove', handleMove);
};
}, []); // 空依赖,只在挂载/卸载时执行
return position;
}
现在,在任何组件里,只要一行代码就能“钓”到鼠标:
function MouseMove() {
const { x, y } = useMouse();
return <div>🐭 鼠标位置:{x}, {y}</div>;
}
干净、安全、可复用! 这就是自定义 Hook 的魅力。
三、实战第二弹:用 useTodos 管理待办事项 📝
接下来,我们挑战更复杂的逻辑:一个完整的 TodoList,包含添加、删除、完成/未完成切换。
如果把这些逻辑全塞进 App 组件?代码会迅速膨胀成“意大利面条”。
不如——把它抽成 useTodos Hook!
// hooks/useTodos.js
import { useState } from 'react';
export function useTodos() {
const [todos, setTodos] = useState([
{ id: 1, text: '学习 React Hooks', completed: false }
]);
const addTodo = (text) => {
const newTodo = {
id: Date.now(), // 简单 ID 生成(生产环境请用 uuid)
text,
completed: false
};
setTodos([...todos, newTodo]);
};
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
};
}
看!状态 + 操作逻辑 全封装在一个 Hook 里。
在 App 中使用时,简直清爽到飞起:
// App.jsx
import { useTodos } from './hooks/useTodos';
import TodoInput from './components/TodoInput';
import TodoList from './components/TodoList';
export default function App() {
const { todos, addTodo, toggleTodo, deleteTodo } = useTodos();
return (
<>
<h1>✨ 我的待办清单</h1>
<TodoInput addTodo={addTodo} />
{todos.length > 0 ? (
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
) : (
<p>🎉 恭喜!没有待办事项了~</p>
)}
</>
);
}
每个子组件只负责 UI 渲染,逻辑全交给 Hook——关注点分离,代码可读性拉满!
四、为什么自定义 Hook 是 React 开发者的“瑞士军刀”?
- 复用逻辑,不是 UI
Mixin 和 HOC 复用的是组件结构,而 Hook 复用的是状态逻辑本身。比如useMouse可以用在任何需要追踪鼠标的场景。 - 避免“Wrapper Hell”
不再需要层层嵌套的高阶组件,代码扁平如草原。 - 测试友好
自定义 Hook 可以单独测试(配合@testing-library/react-hooks),不用渲染整个组件树。 - 组合优于继承
你可以把多个 Hook 组合起来:useMouse+useLocalStorage+useDebounce……像搭积木一样构建复杂功能。
五、小彩蛋:条件渲染 + Hook 的“坑”
注意这段被注释掉的代码:
{count % 2 === 0 && <MouseMove />}
如果取消注释,你会发现:每次 count 变化导致 MouseMove 卸载/重挂,鼠标位置会重置为 (0,0) 。
为什么?因为 useMouse 的 useEffect 在组件卸载时清理了监听器,重挂时重新初始化状态。
这不是 Bug,而是符合预期的行为!
但也提醒我们:不要随意条件渲染带状态的组件,除非你真的希望它“重置”。
六、结语:Hook 之道,在于“组合”与“克制”
React Hooks 强大,但不是万能胶水。
好的 Hook 应该:
- 职责单一(比如
useMouse只管鼠标位置) - 命名清晰(一看就知道用途)
- 自动处理副作用清理(防内存泄漏!)
- 不过度抽象(别为了 Hook 而 Hook)
最后送大家一句话:
“当你觉得某个逻辑在多个组件里重复出现,就是时候写个 useXXX 了。”