你是不是经常在多个组件里写同样的逻辑?比如监听窗口大小变化,或者跟踪鼠标位置?每次都要写一遍useEffect添加监听,还要在卸载时清理事件… 这样的重复代码不仅浪费时间,还容易出错。今天我要分享的React自定义Hook,就是解决这个痛点的神器!它能让你像搭积木一样封装和复用组件逻辑,代码整洁度直接提升一个level!
什么是React自定义Hook?
先看一个实际例子:
让我直接用一个获取窗口大小的例子来展示自定义Hook的魅力。没有自定义Hook之前,你可能这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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后,代码变得超简洁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import React from'react';
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
鼠标位置跟踪是另一个常见需求,比如实现拖拽效果、鼠标悬停提示等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| import { useState, useEffect } from'react';
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); }; }, []);
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官方强制要求的约定!原因有三:让React工具能识别HookReact DevTools等调试工具依赖use前缀来识别哪些函数是Hook,这样在调试时能正确显示Hook的状态和调用顺序。避免违反Hook规则ESLint的React Hook插件会检查以use开头的函数,确保它们遵守Hook的规则。如果你不用use开头,这些重要的静态检查就失效了。提高代码可读性看到use开头,其他开发者(包括未来的你)立即知道这是一个自定义Hook,而不是普通函数。
1 2 3 4 5 6 7 8 9 10
| function useLocalStorage(key, initialValue) { }
function getLocalStorage(key, initialValue) { }
|
记住这个简单的规则:自定义Hook的名字必须以use开头,后面跟描述性的名称,使用驼峰命名法。
实战:组合多个自定义Hook实现复杂功能
真正的威力在于,你可以像搭积木一样组合多个自定义Hook!比如,我们要实现一个响应式的组件,在桌面端显示详细内容,在移动端显示简化版,并且根据鼠标位置改变样式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function BadComponent({ shouldTrack }) { if (shouldTrack) { const position = useMousePosition(); }
return <div>错误示例</div>; }
function GoodComponent({ shouldTrack }) { const position = useMousePosition();
useEffect(() => { if (shouldTrack) { console.log('跟踪位置:', position); } }, [shouldTrack, position]);
return <div>正确示例</div>; }
|
为什么有这个规则?React依赖Hook的调用顺序来正确管理状态。如果条件不同导致Hook调用顺序变化,状态就会乱套。
规则2:只在React函数中调用Hook
在React函数组件或自定义Hook中调用,不要在普通JavaScript函数中调用
1 2 3 4 5 6 7 8 9 10 11 12 13
| function regularFunction() { const [value, setValue] = useState(''); }
function MyComponent() { const [value, setValue] = useState(''); }
function useCustomHook() { const [value, setValue] = useState(''); }
|
规则3:自定义Hook必须返回需要的内容
根据需要返回状态、函数或其他值,保持接口清晰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| function useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.log(error); return initialValue; } });
const setValue = (value) => { try { const valueToStore = value instanceofFunction ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.log(error); } };
return [storedValue, setValue]; }
function UserSettings() { 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| 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) { thrownewError(`HTTP错误: ${response.status}`); } const result = await response.json(); setData(result); } catch (err) { setError(err.message); } finally { setLoading(false); } };
fetchData(); }, [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也可能引入性能问题,特别是当返回对象或数组时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function useUserInfo(userId) { const [user, setUser] = useState(null);
return { user, isAdmin: user?.role === 'admin', canEdit: user?.permissions?.includes('edit') }; }
function useUserInfoOptimized(userId) { const [user, setUser] = useState(null);
const userInfo = useMemo(() => ({ user, isAdmin: user?.role === 'admin', canEdit: user?.permissions?.includes('edit') }), [user]);
return userInfo; }
|
测试自定义Hook:确保可靠性
自定义Hook也需要测试!推荐使用@testing-library/react-hooks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { renderHook, act } from'@testing-library/react-hooks'; import { useCounter } from'./useCounter';
test('useCounter应该正确增加计数', () => { const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
act(() => { result.current.increment(); });
expect(result.current.count).toBe(1); });
|
总结:什么时候该用自定义Hook?
经过这么多例子,你可能想问:到底什么时候应该创建自定义Hook?
适合使用自定义Hook的场景:
多个组件共享相同的状态逻辑
复杂的useEffect逻辑需要封装
想要分离关注点,让组件更专注于UI
需要复用数据获取、事件监听等副作用逻辑
不适合的情况:
逻辑只在一个组件中使用,且不太可能复用
逻辑非常简单,封装反而增加复杂度
只是简单的工具函数,不涉及React状态或生命周期
自定义Hook是React函数组件的终极武器,它让逻辑复用变得前所未有的简单。从今天开始,试着把你项目中的重复逻辑提取成自定义Hook吧!
Prev: Vue 2 源码解读
Next: useState函数怎么实现的