创作者:Yardon |GitHub:github.com/YardonYan |版本:v1.0
为什么需要自定义 Hook
假设你在三个不同的页面都要做一个功能:用户输入搜索词后,等 300ms 没动静了才发请求(防抖)。
你可以在每个页面都写一遍:
function SearchPage1() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); useEffect(() => { const timeout = setTimeout(() => { if (query) fetchResults(query).then(setResults); }, 300); return () => clearTimeout(timeout); }, [query]); // ... 这段代码要复制三份! }这就是典型的代码重复。防抖逻辑本身是独立的,完全可以抽出来。而且当产品说"改成 500ms"时,你只要改一个地方。
自定义 Hook 就是为这个场景设计的——把可复用的逻辑封装成函数,这个函数内部可以用其他 Hook。
自定义 Hook 的基本结构
自定义 Hook 本质上就是一个普通的 JavaScript 函数,函数名以use开头,内部可以调用其他 Hook。
functionuseDebounce(value,delay=300){const[debouncedValue,setDebouncedValue]=useState(value);useEffect(()=>{consttimeout=setTimeout(()=>{setDebouncedValue(value);},delay);return()=>clearTimeout(timeout);},[value,delay]);returndebouncedValue;}这就是一个 Hook。它用到了useState和useEffect,所以它自己也是个 Hook(React 的规则:只有 Hook 才能调用其他 Hook)。
用法
function SearchPage() { const [query, setQuery] = useState(''); const debouncedQuery = useDebounce(query, 300); useEffect(() => { if (debouncedQuery) fetchResults(debouncedQuery); }, [debouncedQuery]); return <input value={query} onChange={(e) => setQuery(e.target.value)} />; }现在防抖逻辑只在一处定义了,三个页面都可以用。
经典案例:useDebounce
继续深化这个案例,加上更多防抖的变体:
functionuseDebouncedValue(value,delay=300){const[debouncedValue,setDebouncedValue]=useState(value);useEffect(()=>{consttimer=setTimeout(()=>{setDebouncedValue(value);},delay);return()=>clearTimeout(timer);},[value,delay]);returndebouncedValue;}// 防抖版表单:用户停止输入后才更新functionuseDebouncedForm(initialValues,delay=300){const[values,setValues]=useState(initialValues);// 为每个字段单独防抖constdebouncedValues={};for(constkeyinvalues){debouncedValues[key]=useDebouncedValue(values[key],delay);}functionhandleChange(key,value){setValues((prev)=>({...prev,[key]:value}));}return{values,debouncedValues,handleChange};}经典案例:useFetch
数据获取是另一个重复高频的��景:
functionuseFetch(url,options={}){const[data,setData]=useState(null);const[loading,setLoading]=useState(true);const[error,setError]=useState(null);useEffect(()=>{constcontroller=newAbortController();asyncfunctionfetchData(){try{setLoading(true);setError(null);constres=awaitfetch(url,{...options,signal:controller.signal});if(!res.ok)thrownewError(`HTTP${res.status}`);constjson=awaitres.json();setData(json);}catch(err){if(err.name!=='AbortError'){setError(err.message);}}finally{setLoading(false);}}fetchData();return()=>controller.abort();},[url,JSON.stringify(options)]);return{data,loading,error,refetch:()=>/* ... */};}用法变得极其简单
function UserList() { const { data, loading, error } = useFetch('/api/users'); if (loading) return <Spinner />; if (error) return <Error msg={error} />; return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>; }一行代码替代了 30 行重复的请求/加载/错误逻辑。这就是 Hook 的价值。
经典案例:useLocalStorage
把数据存进浏览器本地存储,同时保持和 React 状态的同步:
functionuseLocalStorage(key,initialValue){const[storedValue,setStoredValue]=useState(()=>{try{constitem=window.localStorage.getItem(key);returnitem?JSON.parse(item):initialValue;}catch(error){console.warn(`读取 localStorage key "${key}" 失败:`,error);returninitialValue;}});constsetValue=useCallback((value)=>{try{constvalueToStore=valueinstanceofFunction?value(storedValue):value;setStoredValue(valueToStore);window.localStorage.setItem(key,JSON.stringify(valueToStore));}catch(error){console.warn(`写入 localStorage key "${key}" 失败:`,error);}},[key,storedValue]);return[storedValue,setValue];}用法:记住用户的偏好
function App() { const [theme, setTheme] = useLocalStorage('theme', 'dark'); const [language, setLanguage] = useLocalStorage('language', 'zh-CN'); // 用户刷新页面后,主题和语言自动恢复 return <ThemeProvider theme={theme}>...</ThemeProvider>; }Hook 组合:更复杂的逻辑
你可以把多个自定义 Hook 组合在一起,形成更强大的逻辑:
// 一个组合 Hook:用户搜索 + 防抖 + 缓存functionuseSearch(query,options={}){const{baseUrl='/api/search',delay=300,cache=true}=options;constdebouncedQuery=useDebounce(query,delay);constcacheKey=`${baseUrl}:${debouncedQuery}`;const[cachedData,setCachedData]=useLocalStorage(`search-cache`,{});const[freshData,setFreshData]=useState(null);// 优先用缓存constdata=cache&&cachedData[cacheKey]?cachedData[cacheKey]:freshData;useEffect(()=>{if(!debouncedQuery)return;// 检查缓存constcached=cachedData[cacheKey];if(cached&&Date.now()-cached.timestamp<5*60*1000){// 5 分钟内的缓存直接用return;}// 发新请求fetch(`${baseUrl}?q=${encodeURIComponent(debouncedQuery)}`).then((r)=>r.json()).then((d)=>{setFreshData(d);// 更新缓存setCachedData((prev)=>({...prev,[cacheKey]:{...d,timestamp:Date.now()}}));});},[debouncedQuery,baseUrl]);return{data,isLoading:!debouncedQuery||!data};}这就是所谓的「管道式」架构——每个 Hook 只做一件事,组合起来就拥有了完整功能。
测试自定义 Hook
自定义 Hook 的测试需要一点特殊处理——React Testing Library 专门为 Hook 提供了renderHook:
import{renderHook,act}from'@testing-library/react';test('useDebounce 应该延迟返回新值',()=>{const{result,rerender}=renderHook(({value,delay})=>useDebounce(value,delay),{initialProps:{value:'hello',delay:300}});expect(result.current).toBe('hello');// 修改值rerender({value:'world',delay:300});expect(result.current).toBe('hello');// 还没到 300ms// 等 300msjest.advanceTimersByTime(300);expect(result.current).toBe('world');});本章小结
| 概念 | 一句话总结 |
|---|---|
| 自定义 Hook | 以use开头的函数,内部可调用其他 Hook |
| useDebounce | 延迟更新值,常用于搜索输入 |
| useFetch | 封装请求逻辑,一行代码搞定数据获取 |
| useLocalStorage | 持久化状态到浏览器本地存储 |
| Hook 组合 | 把多个简单 Hook 组合成复杂逻辑 |
自定义 Hook 把可复用的逻辑抽离出来——这是 React 应用架构的核心技能。下一章我们聊状态管理——当组件树越来越深时,怎么让状态在任意位置都能访问。
📌创作者:Yardon | 🏠个人网站:GlimmerAI.top
📖 本章是「React 从入门到生产」系列的第 4 章。上一章:副作用与数据获取 | 下一章:状态管理选型
🌟 如果你觉得有帮助,欢迎访问 GlimmerAI.top 查看我的更多作品。欢迎大家来观看!