React Hoo-ks 使用指南-常见场景与性能优化案例_第1页
React Hoo-ks 使用指南-常见场景与性能优化案例_第2页
React Hoo-ks 使用指南-常见场景与性能优化案例_第3页
React Hoo-ks 使用指南-常见场景与性能优化案例_第4页
React Hoo-ks 使用指南-常见场景与性能优化案例_第5页
已阅读5页,还剩36页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

ReactHooks

使用指南

常见场景与性能优化案例

涵盖React16.8至19.x全版本核心Hooks

20+实战案例·15个避坑指南·完整性能优化链路

技术实战系列

目录

第一章ReactHooks核心原理与设计哲学

第二章状态管理场景:useState与useReducer

第三章副作用管理场景:useEffect全解析

第四章性能优化场景:memo/useMemo/useCallback

第五章并发特性场景:useTransition与useDeferredValue

第六章自定义Hook设计模式与实战

第七章React19新Hooks实战

第八章性能优化完整案例:从诊断到解决

第九章常见错误与避坑指南

第十章Hooks速查表与常见问题解答

ReactHooks使用指南·常见场景与性能优化案例

第一章ReactHooks核心原理与设计哲学

ReactHooks是React16.8版本引入的一组API,它允许开发者在函数组件中使用状态、生命周期、副

作用等原本只在类组件中才具备的能力。理解Hooks的核心原理,是正确使用它们的前提。

1.1为什么需要Hooks

在Hooks出现之前,React开发者面临三个根本性痛点。第一,类组件中生命周期方法

(componentDidMount、componentDidUpdate、componentWillUnmount)将相互关联的逻辑拆散在不

同方法中,比如一个数据订阅的"建立"在componentDidMount,"更新"在componentDidUpdate,"清

理"在componentWillUnmount。第二,类组件中this的绑定问题频繁引发bug,事件处理函数必须手动

绑定this或者使用箭头函数。第三,状态逻辑难以在组件之间复用,高阶组件(HOC)和RenderProps

模式虽然可以复用逻辑,但会导致组件树嵌套过深,产生所谓的"嵌套地狱"。

Hooks的设计目标正是解决这三个问题:让逻辑按"关注点"组织而非按"生命周期"组织;消除this的

使用;让状态逻辑可以通过自定义Hook灵活复用。Hooks的底层实现基于一个单向链表结构,每个Hook

调用在链表中占据一个节点,React通过调用顺序来匹配每次渲染的Hook状态。这就是为什么Hook必须遵

循"只在函数组件顶层调用"的规则——一旦调用顺序被打乱,链表匹配就会出错。

1.2RulesofHooks(Hook使用规则)

Hooks有两条不可违背的规则,违反任何一条都会导致不可预期的行为或直接报错。

规则一:只在函数组件或自定义Hook的顶层调用Hook。不要在循环、条件判断或嵌套函数中调

用Hook。这意味着以下写法是错误的:

//❌错误:在条件判断中调用Hook

functionBadComponent({isEnabled}){

if(isEnabled){

const[count,setCount]=useState(0);//违反规则!

}

return<div>...</div>;

}

//✅正确:始终在顶层调用

functionGoodComponent({isEnabled}){

const[count,setCount]=useState(0);

return<div>{isEnabled?count:0}</div>;

}

如果确实需要根据条件执行某些逻辑,应该把条件判断放到Hook内部,而不是把Hook放到条件判断

中:

//✅正确:条件在Hook内部

useEffect(()=>{

if(!isEnabled)return;

//副作用逻辑

return()=>{

//清理逻辑

};

},[isEnabled]);

规则二:只在React函数组件或自定义Hook中调用Hook。不要在普通JavaScript函数中调用

Hook。React依赖调用上下文来识别Hook的归属,普通函数不在React的调用栈中,Hook状态无法被正确

追踪。

为什么顺序如此重要?React用一个数组(或链表)来存储每个组件的Hook状态,每个Hook调用按顺序

对应数组中的一个位置。如果某次渲染跳过了第一个Hook,React会把第二个Hook的状态误当成第一个的,

导致状态错位。这就是条件调用Hook会出问题的根本原因。

1.3Hook的渲染管线:从触发到提交

理解React的渲染管线对于高效使用Hooks至关重要。React的每次更新经历三个阶段:

阶段执行内容可中断

触发状态更新被调度(setState/setCount等调用)—

(Trigger)

渲染调用组件函数、执行useMemo/useReducer计算、key匹配、协调是(并发模式

(Render)(Reconciliation)下)

提交DOM变更、useLayoutEffect、ref赋值、useEffect(异步调度)否

(Commit)

渲染阶段是纯计算过程,React调用组件函数并生成虚拟DOM树。只有协调算法检测到差异时,才会

产生实际的DOM变更。渲染可以很便宜(简单的组件),也可以很昂贵(大型列表的过滤排序)。

useMemo和useCallback的作用正是优化渲染阶段的重复计算。

提交阶段是不可中断的,DOM变更、useLayoutEffect和ref赋值都在这个阶段同步执行。

useEffect的副作用函数则在提交阶段完成后异步调度执行,不阻塞浏览器绘制。理解渲染阶段和提交阶

段的区别,是判断应该在useEffect还是useLayoutEffect中执行副作用的关键依据。

1.4引用相等与memoization的关系

React.memo、useMemo和useCallback都依赖Object.is来比较前后两次渲染的值或依赖项。

对于基本类型(字符串、数字、布尔值),值相等即视为相同。但对于引用类型(对象、数组、函数),即

使内容完全相同,只要引用不同,就被视为变化。

这意味着在组件渲染过程中,每次创建的对象字面量或函数表达式都是一个新引用。如果把这个新引用

传递给一个被React.memo包裹的子组件,子组件的prop比较就会失败,导致不必要的重新渲染。例如:

//❌每次渲染都创建新对象,React.memo失效

<ChildComponentconfig={{theme:'dark',size:'large'}}/>

//✅用useMemo保持引用稳定

constconfig=useMemo(()=>({theme:'dark',size:'large'}),[]);

<ChildComponentconfig={config}/>

核心原则:memoization不是"加得越多越好"。每次useMemo和useCallback本身都有开销——React需要

存储依赖数组、执行比较逻辑。只有当被缓存的计算足够昂贵、或者引用稳定性对子组件渲染有实质影响

时,memoization才有价值。在ReactCompiler已经启用的项目中,这些手动memoization在大多数场景下不再

需要。

第二章状态管理场景:useState与useReducer

2.1useState:基础状态管理

useState是最基础也是最常用的Hook。它接收一个初始值作为参数,返回一个包含两个元素的数

组:当前状态值和更新状态的函数。

import{useState}from'react';

functionCounter(){

const[count,setCount]=useState(0);

return(

<div>

<p>当前计数:{count}</p>

<buttononClick={()=>setCount(count+1)}>增加</button>

</div>

);

}

这段代码中,useState(0)将初始值设为0,返回的count是当前状态值,setCount是更新函

数。点击按钮时,setCount被调用,React会重新渲染组件,count变为新值。

函数式更新:当新状态依赖旧状态

当新状态的计算依赖于前一个状态时,应该使用函数式更新,而不是直接传入新值:

//❌潜在问题:连续调用可能不会如预期累加

setCount(count+1);

setCount(count+1);//可能只加了1

//✅函数式更新:确保每次基于最新状态计算

setCount(prev=>prev+1);

setCount(prev=>prev+1);//正确累加2

React的状态更新是异步的,setCount调用后并不会立即改变count的值,而是将更新排入队列。

函数式更新保证每次计算使用的是队列中最新的状态值,而非闭包中捕获的旧值。

惰性初始化:避免昂贵的初始计算

如果初始状态的获取需要执行昂贵的计算,应该传入一个函数给useState,该函数只在首次渲染时

执行:

//❌每次渲染都执行createInitialState()

const[state,setState]=useState(createInitialState());

//✅只在首次渲染时执行

const[state,setState]=useState(()=>createInitialState());

注意写法上的区别:前者传入的是函数调用结果,后者传入的是函数本身。React检测到传入的是函数

时,会将其作为惰性初始化器,只在组件首次挂载时调用。

对象状态的不可变更新

React不会对状态进行深层合并,当状态是对象时,必须手动展开旧属性并覆盖需要修改的字段:

const[form,setForm]=useState({name:'',email:''});

//❌错误:丢失email字段

setForm({name:'张三'});

//✅正确:展开旧状态再覆盖

setForm(prev=>({...prev,name:'张三'}));

2.2useReducer:复杂状态逻辑的优雅方案

当组件包含多个相互关联的状态字段、状态转换逻辑复杂、或者下一个状态依赖于多个先前状态时,

useReducer比useState更合适。它将状态更新的逻辑集中到一个reducer函数中,使状态变化可预

测、可测试。

import{useReducer}from'react';

functionreducer(state,action){

switch(action.type){

case'increment':

return{count:state.count+1};

case'decrement':

return{count:state.count-1};

case'reset':

return{count:action.payload};

default:

thrownewError(`未知action类型:${action.type}`);

}

}

functionCounter(){

const[state,dispatch]=useReducer(reducer,{count:0});

return(

<div>

<p>计数:{state.count}</p>

<buttononClick={()=>dispatch({type:'increment'})}>+1</button>

<buttononClick={()=>dispatch({type:'decrement'})}>-1</button>

<buttononClick={()=>dispatch({type:'reset',payload:0})}>重置</button>

</div>

);

}

对比维度useStateuseReducer

适用场景简单、独立的状态字段复杂状态转换、多字段联动

状态更新逻辑分散在各处的事件处理函数中集中在reducer函数中

可测试性需测试组件reducer是纯函数,可独立测试

代码体积较小略大(需要定义action类型)

性能优化函数式更新即可可通过dispatch引用稳定优化子组件

2.3状态更新批处理机制

React会将同一事件循环中的多个状态更新合并为一次重新渲染。在React18之前,批处理只发生在

React事件处理器中;React18之后,所有更新(包括Promise、setTimeout、原生事件处理器中的更新)都

会自动批处理。

functionhandleClick(){

setCount(c=>c+1);//不会立即重新渲染

setFlag(f=>!f);//不会立即重新渲染

//React只会重新渲染一次,count和flag同时更新

}

如果需要在DOM更新后立即读取最新状态值:可以使用useRef保存最新值,或者在useEffect中读

取。setState是异步的,调用后立即读取state变量获取的是旧值。

第三章副作用管理场景:useEffect全解析

3.1useEffect的执行时机

useEffect用于在函数组件中执行副作用操作,例如数据获取、订阅、手动修改DOM、日志记录

等。它接收两个参数:一个副作用函数和一个依赖数组。useEffect的执行时机是在浏览器完成DOM更

新之后、下一次渲染之前,属于异步执行,不会阻塞浏览器绘制。

import{useState,useEffect}from'react';

functionUserProfile({userId}){

const[user,setUser]=useState(null);

const[loading,setLoading]=useState(true);

useEffect(()=>{

letcancelled=false;

setLoading(true);

fetchUser(userId).then(data=>{

if(!cancelled){

setUser(data);

setLoading(false);

}

});

return()=>{

cancelled=true;//清理:取消过期的请求

};

},[userId]);//userId变化时重新获取数据

if(loading)return<div>加载中...</div>;

return<div>{user?.name}</div>;

}

3.2依赖数组的三种形态

依赖数组形态执行时机典型用途

不传(无第二个参数)每次渲染后都执行极少使用,通常意味着缺少依赖规划

空数组[]仅在组件挂载时执行一次初始化数据获取、一次性事件绑定

有依赖项[a,b]a或b变化时执行响应式数据获取、事件绑定与解绑

3.3闭包陷阱与staleclosure

闭包陷阱是useEffect最隐蔽的问题。当useEffect的副作用函数引用了组件中的变量,但没有

将该变量加入依赖数组时,副作用函数捕获的是该变量在创建时的值,而不是最新值。

//❌经典闭包陷阱:count永远不会更新

functionCounter(){

const[count,setCount]=useState(0);

useEffect(()=>{

consttimer=setInterval(()=>{

console.log(count);//始终打印0

setCount(count+1);//始终设置为1

},1000);

return()=>clearInterval(timer);

},[]);//count没有被声明为依赖

return<div>{count}</div>;

}

当依赖数组为空时,useEffect只在挂载时执行一次,其中的count被闭包捕获为初始值0。无论

setCount如何更新状态,闭包中的count始终是0。

解决方案有三种。最直接的方式是把count加入依赖数组,但这样会导致定时器频繁重建。更好的

方式是使用函数式更新:

//✅方案一:函数式更新(不需要依赖count)

useEffect(()=>{

consttimer=setInterval(()=>{

setCount(prev=>prev+1);//基于最新状态计算

},1000);

return()=>clearInterval(timer);

},[]);

如果需要读取最新状态但不希望触发副作用重执行,可以使用useRef保存最新值:

//✅方案二:用useRef保存最新值

functionCounter(){

const[count,setCount]=useState(0);

constcountRef=useRef(count);

//每次count变化时更新ref

useEffect(()=>{

countRef.current=count;

},[count]);

useEffect(()=>{

consttimer=setInterval(()=>{

console.log('最新count:',countRef.current);

setCount(prev=>prev+1);

},1000);

return()=>clearInterval(timer);

},[]);

return<div>{count}</div>;

}

React19.2的useEffectEvent:React19.2引入了实验性的useEffectEventAPI,它允许在useEffect中读取

最新的props和state,而无需将它们加入依赖数组。该Hook返回一个稳定引用的函数,函数体内读取的变量

始终是最新值,且不会触发Effect重执行。当项目中可以使用React19.2时,推荐用useEffectEvent替代

useRef的workaround。

3.4useEffect的清理函数

当副作用函数需要清理(取消订阅、清除定时器、取消请求)时,useEffect的回调函数应返回一个

清理函数。React会在组件卸载时以及下一次Effect执行前调用该清理函数。

useEffect(()=>{

constcontroller=newAbortController();

fetch('/api/data',{signal:controller.signal})

.then(res=>res.json())

.then(data=>setData(data))

.catch(err=>{

if(!=='AbortError')setError(err);

});

return()=>{

controller.abort();//组件卸载或依赖变化时取消请求

};

},[query]);

清理函数的执行时机包括两种:一是组件卸载时,二是依赖项变化导致Effect重新执行之前。每次

Effect重新执行前,React会先调用上一次Effect返回的清理函数,再执行新的Effect。

3.5useEffectvsuseLayoutEffect

对比维度useEffectuseLayoutEffect

执行时机浏览器绘制后异步执行DOM更新后、浏览器绘制前同步执行

是否阻塞绘制不阻塞阻塞

适用场景数据获取、订阅、日志测量DOM尺寸、同步更新布局、避免闪烁

服务端渲染不会执行会发出警告(SSR中不可用)

核心判断原则:如果副作用的执行结果会影响用户看到的界面,且不希望出现闪烁,使用

useLayoutEffect;否则优先使用useEffect。

第四章性能优化场景:memo/useMemo/useCallback

4.1React.memo:组件级渲染拦截

React.memo是一个高阶组件,它包裹一个函数组件,在props没有变化时跳过该组件的重新渲染。

它使用浅比较(Object.is)来比较前后两次的props。

import{memo}from'react';

constExpensiveList=memo(functionExpensiveList({items,onItemClick}){

console.log('ExpensiveList渲染');

return(

<ul>

{items.map(item=>(

<likey={item.id}onClick={()=>onItemClick(item)}>

{}

</li>

))}

</ul>

);

});

React.memo适用于渲染开销较大或频繁重新渲染的组件,比如大型列表项、图表组件。不应该给每

个组件都包裹React.memo,因为每次比较本身也有开销,且过度使用会增加代码复杂度而不带来实际收

益。是否使用React.memo应该基于Profiler的实际测量结果。

自定义比较函数

当浅比较不足以满足需求时,可以传入第二个参数作为自定义比较函数:

constMemoizedComponent=memo(

functionMyComponent({user,settings}){

//...

},

(prevProps,nextProps)=>{

//返回true表示不重新渲染

returnprevProps.user.id===nextProps.user.id

&&prevProps.settings.theme===nextProps.settings.theme;

}

);

注意:自定义比较函数只在性能确实成为问题时使用。过度自定义比较逻辑可能导致bug(比如遗漏了某个

应该触发渲染的prop变化),且维护成本高。

4.2useMemo:缓存计算结果

useMemo用于缓存计算函数的返回值,只在依赖项变化时重新计算。它适用于计算开销较大的场景,

比如列表过滤、排序、复杂数据转换。

import{useMemo}from'react';

functionProductList({products,keyword,sortBy}){

constfilteredProducts=useMemo(()=>{

console.log('执行过滤和排序');

returnproducts

.filter(p=>.includes(keyword))

.sort((a,b)=>a[sortBy]-b[sortBy]);

},[products,keyword,sortBy]);//依赖变化时才重新计算

return(

<ul>

{filteredProducts.map(p=>(

<likey={p.id}>{}</li>

))}

</ul>

);

}

除了缓存计算,useMemo的另一个重要用途是保持引用稳定性,避免因每次渲染创建新对象而破坏子

组件的React.memo:

//✅保持对象引用稳定

constconfig=useMemo(()=>({

color:'blue',

fontSize:14,

}),[]);

//✅保持数组引用稳定

constoptions=useMemo(()=>[1,2,3],[]);

反模式:不要用useMemo包裹简单值。useMemo(()=>[1,2,3],[])或useMemo(()=>a+b,[a,

b])这类简单计算,使用useMemo带来的比较和存储开销可能比直接计算还大。判断标准是:计算是否昂贵

(循环、排序、复杂转换)、引用稳定性是否对子组件渲染有影响。

4.3useCallback:缓存函数引用

useCallback是useMemo的函数版本,用于缓存函数引用。它只在依赖项变化时返回新的函数,否

则返回缓存的旧函数。

import{useState,useCallback,memo}from'react';

constSearchInput=memo(functionSearchInput({onSearch}){

console.log('SearchInput渲染');

return<inputonChange={e=>onSearch(e.target.value)}/>;

});

functionApp(){

const[keyword,setKeyword]=useState('');

//✅使用useCallback保持函数引用稳定

consthandleSearch=useCallback((value)=>{

setKeyword(value);

},[]);//无依赖,函数引用永远不变

return<SearchInputonSearch={handleSearch}/>;

}

如果没有useCallback,每次App渲染都会创建新的handleSearch函数,即使SearchInput

被memo包裹,也会因为prop引用变化而重新渲染。

但需要注意:useCallback只有在函数被传递给被memo包裹的子组件、或者作为其他Hook的依

赖项时才有价值。如果一个函数只在当前组件内部使用,且不会导致子组件不必要的渲染,那么使用

useCallback是多余的。

4.4三者协作的典型模式

在实际项目中,React.memo、useMemo和useCallback往往配合使用。以下是一个搜索过滤列表

的完整优化示例:

import{useState,useMemo,useCallback,memo}from'react';

//子组件:用memo包裹

constProductItem=memo(functionProductItem({product,onSelect}){

return(

<lionClick={()=>onSelect(product.id)}>

{}—¥{product.price}

</li>

);

});

functionProductSearch({products}){

const[keyword,setKeyword]=useState('');

const[selectedId,setSelectedId]=useState(null);

//过滤结果缓存

constfilteredProducts=useMemo(

()=>products.filter(p=>.includes(keyword)),

[products,keyword]

);

//事件处理函数缓存

consthandleSelect=useCallback((id)=>{

setSelectedId(id);

},[]);

return(

<div>

<input

value={keyword}

onChange={e=>setKeyword(e.target.value)}

placeholder="搜索商品"

/>

<ul>

{filteredProducts.map(p=>(

<ProductItemkey={p.id}product={p}onSelect={handleSelect}/>

))}

</ul>

<p>已选择:{selectedId}</p>

</div>

);

}

这个例子中,useMemo避免了每次渲染都重新过滤列表,useCallback保证了handleSelect引

用稳定,memo保证了当ProductItem的props未变化时跳过渲染。

第五章并发特性场景:useTransition与

useDeferredValue

5.1并发渲染的核心思想

React18引入的并发特性改变了渲染的调度方式。在并发模式下,React可以将渲染工作拆分为可中断

的片段,优先处理紧急更新(用户输入、点击),将非紧急更新(列表过滤、搜索结果渲染)推迟到浏览器

空闲时执行。这不会减少总CPU开销,但显著提升了用户感知的流畅度。

5.2useTransition:标记非紧急更新

useTransition返回一个包含两个元素的数组:isPending布尔值和startTransition函数。在

startTransition中包裹的状态更新会被标记为"过渡更新",优先级低于紧急更新。

import{useState,useTransition}from'react';

functionSearchPage(){

const[query,setQuery]=useState('');

const[results,setResults]=useState([]);

const[isPending,startTransition]=useTransition();

functionhandleChange(e){

constvalue=e.target.value;

setQuery(value);//紧急更新:输入框立即响应

startTransition(()=>{

//非紧急更新:搜索结果可以延迟

constfiltered=hugeList.filter(item=>

.includes(value)

);

setResults(filtered);

});

}

return(

<div>

<inputvalue={query}onChange={handleChange}placeholder="搜索"/>

{isPending&&<span>搜索中...</span>}

<ul>

{results.map(r=><likey={r.id}>{}</li>)}

</ul>

</div>

);

}

在这个例子中,输入框的值更新是紧急更新,用户希望立即看到自己输入的内容;搜索结果的过滤是过

渡更新,用户对几百毫秒的延迟不敏感。isPending为true时表示过渡更新正在进行,可以用于显示

加载状态。

5.3useDeferredValue:延迟值的更新

useDeferredValue接收一个值,返回该值的"延迟版本"。当原始值快速变化时,延迟版本会滞后更

新,从而避免触发昂贵的重新渲染。

import{useState,useDeferredValue,memo}from'react';

constSlowList=memo(functionSlowList({query}){

//模拟渲染大量数据

constitems=[];

for(leti=0;i<5000;i++){

if(String(i).includes(query)){

items.push(<likey={i}>{i}</li>);

}

}

return<ul>{items}</ul>;

});

functionApp(){

const[query,setQuery]=useState('');

constdeferredQuery=useDeferredValue(query);

return(

<div>

<inputvalue={query}onChange={e=>setQuery(e.target.value)}/>

{/*使用延迟值渲染慢列表*/}

<SlowListquery={deferredQuery}/>

</div>

);

}

当用户快速输入时,query立即更新,输入框保持响应;deferredQuery滞后更新,SlowList不

会在每次按键时都重新渲染,而是在用户停止输入后使用最新值渲染一次。

5.4选择依据:useTransitionvsuseDeferredValue

判断条件推荐使用

你控制状态更新函数(可以包裹setState)useTransition

你不控制状态更新函数(值来自props或外部库)useDeferredValue

需要isPending状态来显示加载指示器useTransition

只想延迟某个值的渲染,不需要pending状态useDeferredValue

多个状态需要同时标记为过渡更新useTransition

重要提醒:useTransition和useDeferredValue只对CPU密集型的状态更新有效(如大数据量的过滤、

排序、渲染)。对于网络请求(数据获取),它们不会减少网络延迟,因为网络请求的时间不受React调度控

制。不要用它们来替代数据获取的loading状态管理。

第六章自定义Hook设计模式与实战

6.1自定义Hook的本质

自定义Hook是一个以use开头的JavaScript函数,内部可以调用其他Hook。它不是React的内置功

能,而是一种代码复用的约定。自定义Hook的核心价值是将有状态逻辑从组件中提取出来,使逻辑可以独

立测试、独立演进,并在多个组件之间共享。

与高阶组件(HOC)和RenderProps相比,自定义Hook不会改变组件树的嵌套结构,不会产生"包装

地狱",且类型推导更自然。两个组件使用同一个自定义Hook,它们的状态是相互独立的——每个Hook调

用都有自己独立的state和effect。

6.2实战:useLocalStorage

将状态同步到localStorage是一个常见需求,以下是一个生产级别的实现:

import{useState,useEffect,useCallback}from'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}`,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}`,error);

}

},[key,storedValue]);

//监听其他标签页的变更

useEffect(()=>{

functionhandleStorage(e){

if(e.key===key&&e.newValue!==null){

try{

setStoredValue(JSON.parse(e.newValue));

}catch{

//忽略解析失败

}

}

}

window.addEventListener('storage',handleStorage);

return()=>window.removeEventListener('storage',handleStorage);

},[key]);

return[storedValue,setValue];

}

这个Hook实现了三个关键功能:惰性初始化避免每次渲染都读取localStorage;setValue同时更新

React状态和localStorage;通过监听storage事件实现跨标签页同步。

6.3实战:useDebounce

防抖是搜索输入场景的常见需求,以下是实现:

import{useState,useEffect}from'react';

functionuseDebounce(value,delay=300){

const[debouncedValue,setDebouncedValue]=useState(value);

useEffect(()=>{

consttimer=setTimeout(()=>{

setDebouncedValue(value);

},delay);

return()=>{

clearTimeout(timer);//值变化时清除上一个定时器

};

},[value,delay]);

returndebouncedValue;

}

//使用示例

functionSearchBox(){

const[keyword,setKeyword]=useState('');

constdebouncedKeyword=useDebounce(keyword,500);

useEffect(()=>{

if(debouncedKeyword){

fetchSearchResults(debouncedKeyword);

}

},[debouncedKeyword]);

return<inputvalue={keyword}onChange={e=>setKeyword(e.target.value)}/>;

}

6.4实战:useIntersectionObserver

无限滚动和懒加载场景中,检测元素是否进入视口是核心需求。使用IntersectionObserver可以高

效实现,以下是对应的自定义Hook:

import{useState,useEffect,useRef}from'react';

functionuseIntersectionObserver(options={}){

const[isIntersecting,setIsIntersecting]=useState(false);

consttargetRef=useRef(null);

useEffect(()=>{

constelement=targetRef.current;

if(!element)return;

constobserver=newIntersectionObserver(

([entry])=>{

setIsIntersecting(entry.isIntersecting);

},

{threshold:0.1,...options}

);

observer.observe(element);

return()=>observer.disconnect();

},[options.threshold,options.root,options.rootMargin]);

return[targetRef,isIntersecting];

}

//使用:懒加载列表项

functionInfiniteList({items,onLoadMore}){

const[sentinelRef,isVisible]=useIntersectionObserver();

useEffect(()=>{

if(isVisible)onLoadMore();

},[isVisible,onLoadMore]);

return(

<ul>

{items.map(item=><likey={item.id}>{}</li>)}

<divref={sentinelRef}style={{height:1}}/>

</ul>

);

}

6.5实战:usePrevious

获取某个值在上一次渲染中的值,在动画、比较场景中很有用:

import{useRef,useEffect}from'react';

functionusePrevious(value){

constref=useRef(undefined);

useEffect(()=>{

ref.current=value;

},[value]);

returnref.current;

}

//使用:检测值的变化方向

functionPriceTracker({price}){

constprevPrice=usePrevious(price);

consttrend=prevPrice===undefined?'flat'

:price>prevPrice?'up':price<prevPrice?'down':'flat';

return(

<div>

<span>当前价格:{price}</span>

<span>{trend==='up'?'↑':trend==='down'?'↓':'→'}</span>

</div>

);

}

自定义Hook的设计原则:每个自定义Hook应该只负责一个关注点。不要创建一个包含十几种功能的"万能

Hook",而应该拆分为多个小而专注的Hook,在组件中按需组合。Hook之间可以相互调用,组合是自定义

Hook最强大的能力。

第七章React19新Hooks实战

7.1useActionState:表单状态管理

useActionState(在React18.3中名为useFormState)用于管理表单提交的状态,它与Server

Actions配合使用时特别强大:

import{useActionState}from'react';

asyncfunctionsubmitForm(prevState,formData){

constname=formData.get('name');

constemail=formData.get('email');

try{

awaitapi.register({name,email});

return{success:true,message:'注册成功'};

}catch(error){

return{success:false,message:error.message};

}

}

functionRegisterForm(){

const[state,formAction,isPending]=useActionState(submitForm,null);

return(

<formaction={formAction}>

<inputname="name"placeholder="姓名"required/>

<inputname="email"type="email"placeholder="邮箱"required/>

<buttontype="submit"disabled={isPending}>

{isPending?'提交中...':'注册'}

</button>

{state&&(

<pstyle={{color:state.success?'green':'red'}}>

{state.message}

</p>

)}

</form>

);

}

useActionState自动管理了表单提交过程中的pending状态、错误处理和结果反馈,避免了手动维护

多个useState的繁琐。

7.2useFormStatus:子组件感知表单状态

useFormStatus必须在<form>的子组件中使用,用于读取父级表单的提交状态,无需通过props

传递pending标志:

import{useFormStatus}from'react-dom';

functionSubmitButton(){

const{pending}=useFormStatus();

return(

<buttontype="submit"disabled={pending}>

{pending?'提交中...':'提交'}

</button>

);

}

functionMyForm(){

return(

<formaction={submitAction}>

<inputname="title"/>

<SubmitButton/>{/*自动感知表单状态*/}

</form>

);

}

7.3useOptimistic:乐观更新

useOptimistic允许在等待服务端响应期间立即更新UI,如果请求失败则自动回滚:

import{useOptimistic,useState}from'react';

functionTodoList({todos}){

const[optimisticTodos,addOptimisticTodo]=useOptimistic(

todos,

(currentTodos,newTodo)=>[

...currentTodos,

{id:crypto.randomUUID(),text:newTodo,sending:true},

]

);

asyncfunctionhandleAdd(formData){

consttext=formData.get('text');

addOptimisticTodo(text);//立即更新UI

awaitaddTodoToServer(text);//实际请求

}

return(

<div>

<formaction={handleAdd}>

<inputname="text"/>

<buttontype="submit">添加</button>

</form>

<ul>

{optimisticTodos.map(todo=>(

<likey={todo.id}style={{opacity:todo.sending?0.5:1}}>

{todo.text}

</li>

))}

</ul>

</div>

);

}

用户添加待办事项后,列表立即更新(条目半透明显示),服务端请求完成后转为不透明;如果请求失

败,条目会自动消失并回滚到之前的状态。

7.4use新原语

use是React19引入的原语,它既可以读取Promise的值,也可以读取Context,且支持条件调用

(与普通Hook不同):

import{use,Suspense}from'react';

functionComments({commentsPromise}){

constcomments=use(commentsPromise);//读取Promise

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论