React自定义Hooks实战:从封装到复用,彻底解放组件逻辑
在React开发中,你一定用过useState、useEffect这些内置Hooks,它们帮我们摆脱了类组件的繁琐,让函数组件也能拥有状态和生命周期。但当项目越来越复杂,你会发现:多个组件会出现重复的状态逻辑和业务处理,比如监听鼠标位置、管理待办事项、请求接口数据等。
这时候,自定义Hooks就该登场了。它不是React官方提供的,而是我们基于内置Hooks封装的“业务工具”——以use开头,呼之即来、复用性极强,能让组件只关注UI渲染,彻底解放重复逻辑。
今天就以两个高频实战案例(鼠标位置监听、待办事项管理)为核心,从“是什么、为什么、怎么封装、怎么用”四个维度,彻底吃透自定义Hooks,新手也能快速上手封装自己的Hooks。
一、先搞懂:自定义Hooks到底是什么?
1. 核心定义
自定义Hooks是遵循React Hooks规则、以use开头的函数,内部可以调用React内置Hooks(如useState、useEffect),目的是封装可复用的状态逻辑,让组件代码更简洁、更易维护。
2. 两个核心特征(必记,避免踩坑)
- 命名必须以
use开头:这是React的强制约定,不仅能让我们一眼识别出这是Hooks,还能让ESLint自动检查Hooks的使用规则(比如不能在if/for中调用)。 - 内部可以调用其他Hooks:自定义Hooks的本质是“逻辑封装”,依托内置Hooks实现状态管理、生命周期监听等功能,不能脱离内置Hooks单独存在。
3. 核心价值(为什么要封装自定义Hooks?)
在日常React开发中,我们能直观感受到自定义Hooks的3个核心价值:
- 逻辑复用:比如
useMouse封装的鼠标位置监听逻辑,只要在需要的组件中调用,就能快速获得鼠标坐标,无需重复写事件监听、清除监听的代码。 - 组件解耦:组件只负责“UI渲染”(HTML+CSS),复杂的业务逻辑(如待办的增删改查、本地存储)都封装在Hooks中,代码职责更清晰,后期维护更简单。
- 团队资产:封装好的自定义Hooks可以在整个项目中复用,甚至跨项目复用,成为团队的通用工具,提升开发效率。
举个反例:如果不封装待办管理相关的Hooks,就需要把待办的增删改查、本地存储逻辑,全部写在入口组件中,会导致组件代码臃肿、逻辑混乱,其他组件需要用到待办功能时,只能复制粘贴代码——这就是自定义Hooks要解决的核心痛点。
二、实战封装1:useMouse——响应式监听鼠标位置
第一个案例,我们封装useMouse,实现“响应式监听鼠标位置”的功能。这个案例能帮我们掌握自定义Hooks的基础封装逻辑,以及内存泄漏的避免方法,是新手入门自定义Hooks的绝佳案例。
1. 需求分析
实现一个Hooks,能够监听鼠标在页面上的移动,实时返回鼠标的x(水平坐标)和y(垂直坐标),并且在组件卸载时,自动清除事件监听,避免内存泄漏。
2. 完整封装代码(可直接复用)
import { useState, useEffect } from 'react';
// 自定义Hooks:封装响应式鼠标位置监听逻辑
// 命名以use开头,符合React Hooks约定
export const useMouse = () => {
// 1. 用useState维护鼠标坐标状态,初始值为0
const [x, setX] = useState(0);
const [y, setY] = useState(0);
// 2. 用useEffect监听mousemove事件(生命周期逻辑)
useEffect(() => {
// 定义事件处理函数:更新鼠标坐标
const update = (event) => {
setX(event.pageX); // 更新水平坐标
setY(event.pageY); // 更新垂直坐标
}
// 给window绑定mousemove事件,触发时执行update
window.addEventListener('mousemove', update);
// 3. 清除副作用:组件卸载时,移除事件监听(避免内存泄漏)
// useEffect的return函数会在组件卸载时执行
return () => {
window.removeEventListener('mousemove', update);
}
}, []) // 空依赖数组:只在组件挂载时绑定一次事件,卸载时清除
// 4. 返回需要暴露给组件的状态(x、y坐标)
 return {
 x,
 y
 }
}
3. 逐行拆解封装逻辑(新手必看)
- 状态定义:用
useState(0)定义x和y两个状态,分别保存鼠标的水平和垂直坐标,初始值都为0。 - 事件监听:在
useEffect中给window绑定mousemove事件,每次鼠标移动时,执行update函数,通过setX和setY更新坐标状态——这是Hooks中处理“副作用”(如事件监听、定时器)的标准方式。 - 避免内存泄漏(重点) :组件卸载时,若不清除
mousemove事件监听,事件回调函数会一直存在,导致内存泄漏(页面占用内存越来越大)。而useEffect的return函数,会在组件卸载时自动执行,我们在这里调用removeEventListener,就能彻底清除监听,避免内存泄漏。 - 返回状态:将
x和y封装成对象返回,供组件调用——组件只需调用useMouse(),就能获取到实时的鼠标坐标。
4. 组件中使用useMouse(极简)
封装完成后,组件使用起来非常简单,无需关注事件监听和清除的逻辑,只需要渲染UI即可,以下是极简使用示例:
import { useMouse } from './hooks/useMouse.js';
function MouseMove() {
// 调用自定义Hooks,获取鼠标坐标
const { x, y } = useMouse();
// 只关注UI渲染,无需处理任何业务逻辑
return (
<>
<div>
鼠标位置:{x} {y}
</div>
</>
)
}
效果:鼠标在页面上移动时,div会实时显示当前鼠标的坐标,组件卸载时,事件监听会自动清除,不会造成内存泄漏。
三、实战封装2:useTodos——完整待办事项管理
第二个案例,我们封装useTodos,实现“待办事项的增删改查+本地存储”的完整功能。这个案例能帮我们掌握更复杂的业务逻辑封装,以及useState函数式更新、本地存储的结合使用,贴合实际项目开发场景。
1. 需求分析
实现一个Hooks,负责管理待办事项的全部逻辑:
- 初始化:从本地存储(localStorage)加载待办列表;
- 核心操作:添加待办、切换待办完成状态、删除待办;
- 持久化:待办列表变化时,自动同步到本地存储,刷新页面数据不丢失;
- 暴露状态和方法:给组件提供待办列表(
todos)和操作方法(addTodo、toggleTodo、deleteTodo)。
2. 完整封装代码(可直接复用)
import { useState, useEffect } from 'react';
// 定义本地存储的key,单独提取,方便维护
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));
}
// 自定义Hooks:封装待办事项管理的完整逻辑
export const useTodos = () => {
// 1. 初始化状态:从本地存储加载待办列表(useState接收函数,同步计算初始值)
const [todos, setTodos] = useState(loadFromStorage);
// 2. 监听todos变化,同步到本地存储(持久化)
useEffect(() => {
saveToStorage(todos);
}, [todos]) // 依赖todos:只要todos变化,就执行保存操作
// 3. 封装添加待办的方法
const addTodo = (text) => {
// 函数式更新:prevTodos是上一次的todos状态,确保拿到最新值
setTodos(prevTodos => [
...prevTodos, // 保留原有待办
{
id: Date.now(), // 用时间戳生成唯一ID(避免重复)
text, // 待办内容
completed: false // 初始状态:未完成
}
])
}
// 4. 封装切换待办完成状态的方法
const toggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo => {
// 找到对应ID的待办,切换completed状态
if(todo.id === id) {
return { ...todo, completed: !todo.completed };
}
return todo; // 其他待办不变
})
)
}
// 5. 封装删除待办的方法
const deleteTodo = (id) => {
setTodos(prevTodos =>
// 过滤掉要删除的待办,返回新数组
prevTodos.filter(todo => todo.id !== id)
)
}
// 6. 返回待办列表和操作方法,供组件使用
return {
todos,
addTodo,
toggleTodo,
deleteTodo
}
}
3. 核心封装细节拆解(新手避坑)
- 本地存储封装:将“加载”和“保存”本地存储的逻辑,提取为
loadFromStorage和saveToStorage两个辅助函数,代码更简洁、可维护——如果后续需要修改存储方式(比如改用sessionStorage),只需修改这两个函数即可。 - useState函数式更新:添加、修改、删除待办时,都用了
setTodos(prevTodos => { ... })的形式,而不是直接使用todos。这是因为新的待办状态依赖于旧的状态,函数式更新能确保我们拿到的是最新的状态值,避免出现“状态更新不及时”的bug。 - 持久化逻辑:通过
useEffect监听todos的变化,只要todos发生改变(添加、删除、切换状态),就会自动调用saveToStorage,将最新的待办列表保存到本地存储——这样即使页面刷新,待办数据也不会丢失。 - 唯一ID生成:用
Date.now()生成待办的id,这是一种简单高效的唯一ID生成方式(时间戳毫秒级,人类操作间隔远大于1毫秒,不会重复),适合小型项目使用。
4. 组件中使用useTodos(联动组件实战)
封装完成后,我们可以结合TodoInput、TodoList、TodoItem三个组件,实现完整的待办功能——组件只需专注于UI渲染和事件传递,无需处理任何待办业务逻辑,真正实现逻辑与UI解耦。
(1)App组件(入口组件,整合所有功能)
import { useTodos } from './hooks/useTodos.js';
import TodoList from './components/TodoList.jsx';
import TodoInput from './components/TodoInput.jsx';
export default function App() {
// 调用useTodos,获取待办列表和操作方法
const {
todos,
addTodo,
deleteTodo,
toggleTodo
} = useTodos();
return (
<>
{/* 待办输入框:传递addTodo方法,用于添加待办 */}
<TodoInput onAddTodo={addTodo}/>
{/* 待办列表:有数据则渲染列表,无数据则提示 */}
{
todos.length > 0 ?(
<TodoList
onDelete={deleteTodo} // 传递删除方法
onToggle={toggleTodo} // 传递切换状态方法
todos={todos} // 传递待办列表
/>
):(
<div>暂无待办事项</div>
)
}
</>
)
}
(2)TodoInput组件(输入待办,触发添加)
import { useState } from 'react';
// 接收父组件传递的addTodo方法
export default function TodoInput({ onAddTodo }) {
const [text, setText] = useState(''); // 管理输入框文本状态
// 表单提交事件:添加待办
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表单默认提交行为
if (!text.trim()) return; // 空文本不添加,避免无效数据
onAddTodo(text.trim()); // 调用addTodo方法,添加待办
setText(""); // 清空输入框
}
return (
<form className="todo-input" onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={e => setText(e.target.value)} // 实时更新输入框文本
/>
</form>
)
}
(3)TodoList和TodoItem组件(渲染待办,触发操作)
// TodoList.jsx:渲染待办列表,遍历生成TodoItem
import TodoItem from './TodoItem.jsx';
export default function TodoList({ todos, onDelete, onToggle }) {
return (
<ul className="todo-list">
{
todos.map(todo => (
<TodoItem
key={todo.id} // 唯一key,避免React警告
todo={todo} // 传递单个待办数据
onDelete={onDelete} // 传递删除方法
onToggle={onToggle} // 传递切换状态方法
/>
))
}
</ul>
)
}
// TodoItem.jsx:渲染单个待办项,绑定操作事件
export default function TodoItem({todo, onToggle, onDelete}) {
return (
<li className="todo-item">
{/* 复选框:绑定切换待办状态事件 */}
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{/* 待办文本:完成状态添加completed样式 */}
<span className={todo.completed ? 'completed' : ''}>
{todo.text}
</span>
{/* 删除按钮:绑定删除事件 */}
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
)
}
5. 效果说明
- 用户在输入框输入文本,提交后会添加一条待办事项,同时同步到本地存储;
- 点击复选框,可切换待办的完成状态,状态变化会自动保存到本地存储;
- 点击Delete按钮,可删除对应的待办事项,删除后数据也会同步更新;
- 刷新页面,待办数据会从本地存储加载,不会丢失;
- 所有业务逻辑(增删改查、本地存储)都封装在
useTodos中,组件只负责UI渲染和事件传递,代码简洁、职责清晰。
四、自定义Hooks的核心规则(必遵守,否则出bug)
无论是内置Hooks还是自定义Hooks,都必须遵守React的Hooks规则,否则会导致组件状态混乱、渲染异常等问题,结合实战场景,重点强调3条核心规则:
1. 只能在函数组件或自定义Hooks中调用
不能在普通函数、if/for循环、嵌套函数中调用Hooks。比如:
// ❌ 错误:在if语句中调用useState(自定义Hooks内部调用除外)
function App() {
if (true) {
const [count, setCount] = useState(0);
}
// ...
}
// ✅ 正确:在函数组件顶层或自定义Hooks中调用
function useMouse() {
const [x, setX] = useState(0); // 正确,自定义Hooks内部可调用
// ...
}
2. 调用顺序必须固定
每次组件渲染时,Hooks的调用顺序必须保持一致。比如不能这次先调用useState,下次先调用useMouse——这会导致React无法正确识别每个Hooks的状态。
3. 不要在Hooks中使用非Hooks规则的代码
自定义Hooks内部可以调用其他Hooks,但不能包含组件渲染相关的代码(如JSX),也不能在Hooks中直接操作DOM(除非在useEffect中)。
五、自定义Hooks实战总结(新手必记)
1. 封装思路(三步搞定)
- 明确需求:确定要封装的业务逻辑(比如鼠标监听、待办管理),明确需要暴露给组件的状态和方法;
- 逻辑实现:用内置Hooks(
useState、useEffect等)实现核心逻辑,处理副作用(如事件监听、本地存储),避免内存泄漏; - 暴露接口:将组件需要的状态和方法,以对象或数组的形式返回,供组件调用。
2. 核心避坑点
- 命名必须以
use开头,否则ESLint会报错,也无法被识别为Hooks; - 处理副作用(事件监听、定时器)时,一定要在
useEffect的return函数中清除,避免内存泄漏; - 当新状态依赖旧状态时,一定要用
useState的函数式更新(setState(prev => ...)),确保拿到最新状态; - 自定义Hooks只负责封装逻辑,不负责UI渲染,UI渲染交给组件处理,实现逻辑和UI的解耦。
3. 适用场景
只要多个组件存在重复的状态逻辑,都可以封装成自定义Hooks,常见场景:
- 事件监听类:鼠标位置、滚动位置、窗口大小监听;
- 业务逻辑类:待办管理、购物车管理、用户信息管理;
- 接口请求类:封装接口请求、loading状态、错误处理;
- 工具类:本地存储、定时器、防抖节流等。
自定义Hooks是React开发中的“效率神器”,也是面试高频考点。它不仅能提升代码复用率、简化组件逻辑,还能让你的代码更具可维护性。建议把上面的代码复制到本地运行,亲手修改、调试,感受自定义Hooks的魅力——从模仿封装useMouse、useTodos开始,逐步封装自己的Hooks,让开发更高效!