在 React 项目里,组件一旦开始承担过多逻辑,通常会出现几个明显信号:
- JSX 越来越难读
- 同一类逻辑在多个组件中反复出现
- 生命周期相关代码(事件监听、定时器)分散在各处
- 稍不注意就留下内存泄漏隐患
Hooks 的出现,本质上不是“少写 class”,而是引入了一种函数级别的逻辑抽象方式。
自定义 Hooks,正是这种抽象能力真正发挥价值的地方。
一、Hooks 的本质不是 API,而是函数式拆分
从形式上看,Hooks 只是一些以 use 开头的函数:
useStateuseEffectuseRefuseContext
但从设计角度看,它们做的是同一件事:
把状态和副作用,从组件结构中解耦出来
这也是为什么自定义 Hooks 必须遵循两个规则:
- 必须以
use开头 - 内部可以自由组合 React 内置 Hooks
只要满足这两点,Hooks 就不再依赖具体组件结构,而是可以被任意复用。
二、一个典型问题:mousemove 的响应式实现
先看一个常见需求:
实时获取鼠标在页面中的位置。
如果直接写在组件里,通常会是这样:
useEffect(() => {
const handler = (e) => {
setX(e.pageX);
setY(e.pageY);
};
window.addEventListener('mousemove', handler);
return () => {
window.removeEventListener('mousemove', handler);
};
}, []);
这段代码有几个特点:
- 和 UI 无关
- 强依赖生命周期
- 必须手动清理副作用
如果在多个组件中需要鼠标位置,这段逻辑就会被复制多次。
这正是自定义 Hooks 的切入点。
三、useMouse:把副作用变成可复用能力
将 mousemove 逻辑抽离为一个 Hook:
import { useState, useEffect } from 'react';
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 };
};
这个 Hook 做了三件事:
- 内部维护状态
- 管理事件监听与清理
- 对外只暴露结果
组件不再关心事件绑定细节,只关心数据。
在组件中使用
function MouseMove() {
const { x, y } = useMouse();
return (
<div>
鼠标位置:{x} {y}
</div>
);
}
组件结构变得非常纯粹,只剩下 UI 表达。
四、内存泄漏:Hooks 必须直面的现实问题
一个常见误区是:
组件卸载后,业务函数会自动销毁。
事实上:
- React 只负责卸载组件
- 事件监听、定时器、订阅都需要手动清理
如果在 useEffect 中不返回清理函数,就会出现:
- 事件重复绑定
- 状态更新到已卸载组件
- 内存持续增长
useEffect(() => {}, []) 并不等于“安全”,
只有 return cleanup,副作用才是完整的。
这也是为什么自定义 Hooks 非常适合封装这类逻辑:
一旦写对,所有使用方都自动正确。
五、业务型 Hooks:useTodos 的价值不在于 Todo
再看一个更偏业务的 Hook:Todo 管理。
import { useState, useEffect } from 'react';
const STORAGE_KEY = 'todos';
function loadFromStorage() {
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
}
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
};
};
这里的关键并不在 Todo 本身,而在于:
- 状态初始化逻辑被封装
- 副作用(localStorage 同步)集中管理
- 组件无需关心数据来源
组件只负责组合
export default function App() {
const { todos, addTodo, toggleTodo, deleteTodo } = useTodos();
return (
<>
<TodoInput onAddTodo={addTodo} />
{
todos.length > 0
? <TodoList todos={todos} onToggle={toggleTodo} onDelete={deleteTodo} />
: <div>暂无待办事项</div>
}
</>
);
}
这时,组件更像“配置层”,而不是“逻辑容器”。
六、自定义 Hooks 带来的真实收益
当项目规模变大后,自定义 Hooks 带来的好处会非常明显:
- 逻辑复用不再依赖组件结构
- 副作用集中,内存问题更可控
- UI 与业务彻底解耦
- Hooks 本身可以被测试、被重构
从团队层面看,Hooks 更像一种“逻辑模块”,而不是技巧。
七、什么时候值得抽 Hooks
不是所有逻辑都需要 Hook,但以下场景非常适合:
- 依赖生命周期的逻辑
- 需要清理副作用的逻辑
- 多组件共享的状态行为
- 与 UI 无关的业务流程
一个简单判断标准是:
如果这段代码删除 JSX 后依然成立,它大概率属于 Hook。