React Hooks

发布时间:2026/6/9 18:24:54

React Hooks 文章目录前言一、Hooks 的基本规则1.1 两条核心规则1.2 为什么有这些规则二、常用 Hooks2.1 useState2.2 useEffect2.3 useContext三、useEffect vs useLayoutEffect3.1 执行时机3.2 执行顺序3.3 使用场景四、自定义 Hook4.1 基本规则4.2 自定义 Hook 的设计五、useMemo 与 useCallback5.1 useMemo缓存计算结果5.2 useCallback缓存函数引用5.3 滥用问题六、陈旧闭包问题6.1 问题描述6.2 解决方案七、易混淆点八、思考与练习总结前言上一篇讲了 Diff 算法本篇进入React Hooks——这是 React 16.8 引入的重要特性彻底改变了 React 组件的编写方式。Hooks 解决的核心问题是函数组件没有状态之前只有类组件能管理状态逻辑复用困难HOC 和 Render Props 嵌套地狱生命周期复杂相关逻辑被拆散到不同生命周期方法本篇会讲清楚Hooks 的基本规则常用 Hooks 的使用useEffect 与 useLayoutEffect 的区别自定义 Hook 的设计一、Hooks 的基本规则1.1 两条核心规则// ❌ 错误在条件语句中调用 HookfunctionComponent({flag}){if(flag){const[count,setCount]useState(0)// 错误}}// ❌ 错误在循环中调用 HookfunctionComponent(){for(leti0;i3;i){const[count,setCount]useState(0)// 错误}}// ✅ 正确始终在组件顶层调用 HookfunctionComponent(){const[count,setCount]useState(0)const[name,setName]useState(Alice)// 条件逻辑放在 Hook 之后if(count10){// ...}}规则总结只在最顶层使用 Hook不要在循环、条件或嵌套函数中调用只在 React 函数组件或自定义 Hook 中调用不要在普通函数中调用1.2 为什么有这些规则// React 内部用数组存储 Hook 状态lethooks[]letindex0functionuseState(initialValue){if(hooks[index]undefined){hooks[index]initialValue}constcurrentIndexindexconstsetState(newValue){hooks[currentIndex]newValue}return[hooks[index],setState]}// 每次渲染时Hook 调用顺序必须一致// 否则 index 会错乱导致状态混乱二、常用 Hooks2.1 useStateconst[count,setCount]useState(0)// 函数式更新推荐用于依赖旧值的场景setCount(prevprev1)// 惰性初始化只在首次渲染执行const[state,setState]useState((){returnexpensiveComputation()})2.2 useEffectuseEffect((){// 副作用逻辑consttimersetInterval((){console.log(tick)},1000)// 清理函数在组件卸载或依赖变化前执行return(){clearInterval(timer)}},[deps])// 依赖数组依赖数组规则无依赖每次渲染后都执行空数组[]只在首次渲染后执行有依赖[a, b]依赖变化后执行2.3 useContextconstThemeContextReact.createContext(light)functionApp(){return(ThemeContext.Provider valuedarkChild//ThemeContext.Provider)}functionChild(){constthemeuseContext(ThemeContext)// darkreturndiv className{theme}Hello/div}三、useEffect vs useLayoutEffect3.1 执行时机// useEffect在浏览器完成布局与绘制后异步执行useEffect((){// 不阻塞浏览器渲染})// useLayoutEffect在 DOM 变更后同步执行阻塞渲染useLayoutEffect((){// 阻塞浏览器渲染适合需要同步读取 DOM 布局的场景})3.2 执行顺序functionComponent(){// 1. 组件函数体执行console.log(render)// 2. DOM 更新// 3. 浏览器绘制useLayoutEffect((){console.log(useLayoutEffect)// 先执行})useEffect((){console.log(useEffect)// 后执行})returndivHello/div}// 输出顺序// render// useLayoutEffect// useEffect3.3 使用场景// ✅ useLayoutEffect需要同步测量或修改 DOMfunctionTooltip({targetRef}){const[position,setPosition]useState({x:0,y:0})useLayoutEffect((){constrecttargetRef.current.getBoundingClientRect()setPosition({x:rect.left,y:rect.bottom})},[targetRef])returndiv style{{left:position.x,top:position.y}}Tooltip/div}// ✅ useEffect异步副作用数据请求、订阅、定时器functionUserProfile({userId}){const[user,setUser]useState(null)useEffect((){fetchUser(userId).then(setUser)},[userId])returndiv{user?.name}/div}四、自定义 Hook4.1 基本规则// 自定义 Hook 必须以 use 开头functionuseLocalStorage(key,initialValue){const[value,setValue]useState((){constsavedlocalStorage.getItem(key)returnsaved?JSON.parse(saved):initialValue})useEffect((){localStorage.setItem(key,JSON.stringify(value))},[key,value])return[value,setValue]}// 使用functionApp(){const[name,setName]useLocalStorage(name,Alice)returninput value{name}onChange{esetName(e.target.value)}/}4.2 自定义 Hook 的设计// 封装异步请求逻辑functionuseFetch(url){const[data,setData]useState(null)const[loading,setLoading]useState(true)const[error,setError]useState(null)useEffect((){letcancelledfalseconstfetchDataasync(){setLoading(true)setError(null)try{constresponseawaitfetch(url)constjsonawaitresponse.json()if(!cancelled){setData(json)}}catch(err){if(!cancelled){setError(err)}}finally{if(!cancelled){setLoading(false)}}}fetchData()// 清理函数防止组件卸载后更新状态return(){cancelledtrue}},[url])return{data,loading,error}}// 使用functionUserProfile({userId}){const{data:user,loading,error}useFetch(/api/users/${userId})if(loading)returndivLoading.../divif(error)returndivError:{error.message}/divreturndiv{user.name}/div}五、useMemo 与 useCallback5.1 useMemo缓存计算结果functionApp({items,filter}){// ❌ 每次渲染都重新计算constfilteredItemsitems.filter(itemitem.includes(filter))// ✅ 只在 items 或 filter 变化时重新计算constfilteredItemsuseMemo(()items.filter(itemitem.includes(filter)),[items,filter])returnList items{filteredItems}/}5.2 useCallback缓存函数引用functionApp(){const[count,setCount]useState(0)// ❌ 每次渲染都创建新函数导致子组件重新渲染consthandleClick(){setCount(count1)}// ✅ 缓存函数引用子组件不会因函数变化而重新渲染consthandleClickuseCallback((){setCount(prevprev1)},[])returnButton onClick{handleClick}/}5.3 滥用问题// ❌ 错误过度优化简单计算不需要 useMemoconstfullNameuseMemo(()firstName lastName,[firstName,lastName])// ✅ 正确直接计算constfullNamefirstName lastName// ❌ 错误没有依赖变化的函数不需要 useCallbackconsthandleClickuseCallback((){console.log(click)},[])// 永远不变但增加了复杂性// ✅ 正确只有传递给 memo 子组件时才需要constMemoizedChildReact.memo(Child)functionApp(){consthandleClickuseCallback((){console.log(click)},[])returnMemoizedChild onClick{handleClick}/}六、陈旧闭包问题6.1 问题描述functionCounter(){const[count,setCount]useState(0)useEffect((){consttimersetInterval((){console.log(count)// 永远输出 0setCount(count1)// 永远设置为 1},1000)return()clearInterval(timer)},[])// 依赖数组为空闭包捕获的 count 永远是 0returndiv{count}/div}6.2 解决方案// 方案 1添加依赖useEffect((){consttimersetInterval((){setCount(count1)},1000)return()clearInterval(timer)},[count])// count 变化时重新创建定时器// 方案 2使用函数式更新useEffect((){consttimersetInterval((){setCount(prevprev1)// 使用最新的值},1000)return()clearInterval(timer)},[])// 无需依赖 count// 方案 3使用 useReffunctionCounter(){const[count,setCount]useState(0)constcountRefuseRef(count)countRef.currentcount// 每次渲染更新 refuseEffect((){consttimersetInterval((){console.log(countRef.current)// 读取最新值setCount(countRef.current1)},1000)return()clearInterval(timer)},[])returndiv{count}/div}七、易混淆点useEffect 执行时机在浏览器完成布局与绘制后异步执行不阻塞渲染useLayoutEffect 在 DOM 变更后同步执行阻塞渲染。依赖数组[]表示只执行一次首次渲染后无依赖数组表示每次渲染后都执行。陈旧闭包useEffect 的闭包捕获的是创建时的值不是最新的值。解决方案添加依赖、使用函数式更新、或使用 useRef。useMemo vs useCallbackuseMemo 缓存计算结果useCallback 缓存函数引用。自定义 Hook必须以 “use” 开头本质是复用状态逻辑不是复用状态本身。八、思考与练习1.为什么 Hooks 不能在条件语句中调用解析React 内部用数组存储 Hook 状态依赖调用顺序定位状态。如果在条件语句中调用渲染次数不同会导致调用顺序错乱状态混乱。2.useEffect 和 useLayoutEffect 的区别是什么解析useEffect在浏览器完成布局与绘制后异步执行不阻塞渲染useLayoutEffect在 DOM 变更后同步执行阻塞渲染适用于需要同步测量或修改 DOM 的场景3.什么是陈旧闭包问题如何解决解析// 问题useEffect 的闭包捕获的是创建时的值useEffect((){consttimersetInterval((){setCount(count1)// count 永远是 0},1000)},[])// 解决方案// 1. 添加依赖 [count]// 2. 使用函数式更新 setCount(prev prev 1)// 3. 使用 useRef4.什么时候需要使用 useMemo解析复杂计算过滤、排序、转换大型数组传递给子组件避免子组件因引用变化而重新渲染依赖其他 memoized 值形成 memoized 链简单计算字符串拼接、简单数学运算不需要使用。5.如何设计一个好的自定义 Hook解析单一职责一个 Hook 只做一件事命名清晰以 “use” 开头语义明确参数灵活支持多种配置选项返回值简洁返回对象或数组便于解构// ✅ 好的设计functionuseFetch(url,options{}){const{immediatetrue}options// ...return{data,loading,error,refetch}}// ❌ 不好的设计functionuseData(url,method,headers,body,cache,retry){// 参数过多难以维护}6.useEffect 的清理函数什么时候执行解析组件卸载时组件从 DOM 中移除前依赖变化时下次 effect 执行前不是渲染前不会在首次渲染后执行首次渲染没有上次 effect需要清理总结Hooks 规则只在顶层调用不在条件/循环中调用useEffect异步执行用于副作用数据请求、订阅、定时器useLayoutEffect同步执行用于需要同步测量/修改 DOM 的场景自定义 Hook复用状态逻辑以 “use” 开头useMemo/useCallback缓存计算结果/函数引用避免不必要的重新渲染陈旧闭包闭包捕获的是创建时的值通过依赖、函数式更新或 useRef 解决

相关新闻