你是不是经常在多个组件里写同样的逻辑?比如监听窗口大小变化,或者跟踪鼠标位置?
每次都要写一遍useEffect
添加监听,还要在卸载时清理事件... 这样的重复代码不仅浪费时间,还容易出错。
今天我要分享的React自定义Hook,就是解决这个痛点的神器!它能让你像搭积木一样封装和复用组件逻辑,代码整洁度直接提升一个level!
什么是自定义Hook?先看一个实际例子
让我直接用一个获取窗口大小的例子来展示自定义Hook的魅力。
没有自定义Hook之前,你可能这样写:
jsx
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
// 清理函数:组件卸载时移除监听
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 空依赖数组表示只在组件挂载时执行一次
return (
<div>
当前窗口大小:{windowSize.width} x {windowSize.height}
</div>
);
}
这样写没问题,但如果另一个组件也需要窗口大小呢?难道要复制粘贴一遍?
使用自定义Hook后,代码变得超简洁:
jsx
import React from 'react';
// 自定义Hook:useWindowSize
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return windowSize; // 返回窗口大小状态
}
// 在组件中使用
function MyComponent() {
const windowSize = useWindowSize(); // 一行代码搞定!
return (
<div>
当前窗口大小:{windowSize.width} x {windowSize.height}
</div>
);
}
看到差别了吗?自定义Hook把复杂的逻辑封装起来,使用时只需要一行代码!这才是真正的"写一次,到处用"。
再来看一个跟踪鼠标位置的自定义Hook
鼠标位置跟踪是另一个常见需求,比如实现拖拽效果、鼠标悬停提示等。
jsx
import { useState, useEffect } from 'react';
// 自定义Hook:useMousePosition
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY
});
};
// 监听鼠标移动事件
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []); // 空依赖数组确保effect只运行一次
return position; // 返回鼠标位置
}
// 使用示例
function CursorTracker() {
const mousePos = useMousePosition();
return (
<div>
鼠标当前位置:({mousePos.x}, {mousePos.y})
</div>
);
}
// 另一个组件也可以复用同样的逻辑
function Tooltip() {
const { x, y } = useMousePosition();
return (
<div style={{ position: 'absolute', left: x + 10, top: y + 10 }}>
我是跟随鼠标的提示框!
</div>
);
}
这两个例子展示了自定义Hook的核心价值:把状态逻辑从组件中提取出来,实现真正的逻辑复用。
自定义Hook的命名约定:为什么一定要用use开头?
你可能注意到了,所有自定义Hook都以use
开头,比如useWindowSize
、useMousePosition
。
这可不是随便起的名字,而是React官方强制要求的约定!原因有三:
1. 让React工具能识别Hook React DevTools等调试工具依赖use
前缀来识别哪些函数是Hook,这样在调试时能正确显示Hook的状态和调用顺序。
2. 避免违反Hook规则 ESLint的React Hook插件会检查以use
开头的函数,确保它们遵守Hook的规则。如果你不用use
开头,这些重要的静态检查就失效了。
3. 提高代码可读性 看到use
开头,其他开发者(包括未来的你)立即知道这是一个自定义Hook,而不是普通函数。
jsx
// ✅ 正确:use开头,明确这是自定义Hook
function useLocalStorage(key, initialValue) {
// ... Hook逻辑
}
// ❌ 错误:没有use前缀,React工具无法识别
function getLocalStorage(key, initialValue) {
// 这里如果用了useState等Hook,ESLint会报错
}
记住这个简单的规则:自定义Hook的名字必须以use开头,后面跟描述性的名称,使用驼峰命名法。
实战:组合多个自定义Hook实现复杂功能
真正的威力在于,你可以像搭积木一样组合多个自定义Hook!
比如,我们要实现一个响应式的组件,在桌面端显示详细内容,在移动端显示简化版,并且根据鼠标位置改变样式:
jsx
// 组合使用之前定义的两个Hook
function ResponsiveComponent() {
const windowSize = useWindowSize();
const mousePosition = useMousePosition();
const isMobile = windowSize.width < 768;
const isNearTop = mousePosition.y < 100;
return (
<div style={{
background: isNearTop ? '#f0f0f0' : '#ffffff',
padding: isMobile ? '10px' : '20px'
}}>
{isMobile ? (
<div>移动端简化版</div>
) : (
<div>
<h1>桌面端详细内容</h1>
<p>鼠标位置:({mousePosition.x}, {mousePosition.y})</p>
<p>窗口大小:{windowSize.width} x {windowSize.height}</p>
</div>
)}
</div>
);
}
这种组合能力让代码既简洁又强大,每个Hook专注做好一件事,组件只关心如何组合这些功能。
必须避开的坑:Hook的三大规则
自定义Hook虽然强大,但必须遵守React Hook的基本规则,否则会掉进各种坑里。
规则1:只在最顶层调用Hook
不要在循环、条件或嵌套函数中调用Hook
jsx
// ❌ 错误示例:在条件语句中调用Hook
function BadComponent({ shouldTrack }) {
if (shouldTrack) {
const position = useMousePosition(); // 这样写会出错!
}
return <div>错误示例</div>;
}
// ✅ 正确写法:无条件调用,在effect中控制逻辑
function GoodComponent({ shouldTrack }) {
const position = useMousePosition(); // 始终调用
useEffect(() => {
if (shouldTrack) {
// 在这里使用position做某些事情
console.log('跟踪位置:', position);
}
}, [shouldTrack, position]);
return <div>正确示例</div>;
}
为什么有这个规则? React依赖Hook的调用顺序来正确管理状态。如果条件不同导致Hook调用顺序变化,状态就会乱套。
规则2:只在React函数中调用Hook
在React函数组件或自定义Hook中调用,不要在普通JavaScript函数中调用
jsx
// ❌ 错误:在普通函数中调用Hook
function regularFunction() {
const [value, setValue] = useState(''); // 报错!
}
// ✅ 正确:在组件或自定义Hook中调用
function MyComponent() {
const [value, setValue] = useState(''); // 正确
}
function useCustomHook() {
const [value, setValue] = useState(''); // 正确(自定义Hook也是Hook)
}
规则3:自定义Hook必须返回需要的内容
根据需要返回状态、函数或其他值,保持接口清晰
jsx
// ✅ 好的设计:返回需要的状态和操作方法
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(!value);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
return [value, toggle, setTrue, setFalse]; // 返回数组方便重命名
}
// 使用时的灵活性
function MyComponent() {
const [isOpen, toggleOpen, open, close] = useToggle(false);
return (
<div>
<button onClick={toggleOpen}>切换</button>
<button onClick={open}>打开</button>
<button onClick={close}>关闭</button>
{isOpen && <div>内容</div>}
</div>
);
}
进阶技巧:带参数的自定义Hook
自定义Hook可以接受参数,根据参数不同返回不同的逻辑。
比如,一个增强版的localStorage Hook:
jsx
function useLocalStorage(key, initialValue) {
// 从localStorage读取初始值
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
// 返回的setter函数会同时更新状态和localStorage
const setValue = (value) => {
try {
// 允许value是函数,就像useState一样
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
// 使用示例
function UserSettings() {
// 不同的key创建不同的存储空间
const [username, setUsername] = useLocalStorage('username', '');
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<div>
<input
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="输入用户名"
/>
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</div>
);
}
真实场景:封装数据获取逻辑
数据获取是自定义Hook的经典应用场景。看看如何封装一个通用的数据获取Hook:
jsx
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]); // url变化时重新获取
return { data, loading, error };
}
// 使用示例
function UserProfile({ userId }) {
const { data: user, loading, error } = useApi(`/api/users/${userId}`);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
if (!user) return <div>用户不存在</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
这个useApi
Hook处理了加载状态、错误处理、数据缓存等繁琐细节,让组件代码变得异常简洁。
性能优化:避免不必要的重新渲染
自定义Hook也可能引入性能问题,特别是当返回对象或数组时:
jsx
// ❌ 可能引起性能问题:每次渲染都返回新对象
function useUserInfo(userId) {
const [user, setUser] = useState(null);
// 每次渲染都返回新对象,即使user没变
return {
user,
isAdmin: user?.role === 'admin',
canEdit: user?.permissions?.includes('edit')
};
}
// ✅ 优化版本:使用useMemo缓存计算结果
function useUserInfoOptimized(userId) {
const [user, setUser] = useState(null);
const userInfo = useMemo(() => ({
user,
isAdmin: user?.role === 'admin',
canEdit: user?.permissions?.includes('edit')
}), [user]); // 只有user变化时才重新计算
return userInfo;
}
测试自定义Hook:确保可靠性
自定义Hook也需要测试!推荐使用@testing-library/react-hooks
:
jsx
import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
test('useCounter应该正确增加计数', () => {
const { result } = renderHook(() => useCounter(0));
// 初始值应该是0
expect(result.current.count).toBe(0);
// 测试increment
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
总结:什么时候该用自定义Hook?
经过这么多例子,你可能想问:到底什么时候应该创建自定义Hook?
适合使用自定义Hook的场景:
- 多个组件共享相同的状态逻辑
- 复杂的useEffect逻辑需要封装
- 想要分离关注点,让组件更专注于UI
- 需要复用数据获取、事件监听等副作用逻辑
不适合的情况:
- 逻辑只在一个组件中使用,且不太可能复用
- 逻辑非常简单,封装反而增加复杂度
- 只是简单的工具函数,不涉及React状态或生命周期
自定义Hook是React函数组件的终极武器,它让逻辑复用变得前所未有的简单。从今天开始,试着把你项目中的重复逻辑提取成自定义Hook吧!