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