一文總結 React Hooks 常用場景

關注公眾號?程序員成長指北,回復“1”
??添加無廣告優(yōu)質學習群
前言
文章雖然比較長,但是可以說是全網最全最有用的總結了,學會的記得分享、點贊、收藏、謝謝支持
React 在 v16.8 的版本中推出了 React Hooks 新特性。在我看來,使用 React Hooks 相比于從前的類組件有以下幾點好處:
代碼可讀性更強,原本同一塊功能的代碼邏輯被拆分在了不同的生命周期函數(shù)中,容易使開發(fā)者不利于維護和迭代,通過 React Hooks 可以將功能代碼聚合,方便閱讀維護;
組件樹層級變淺,在原本的代碼中,我們經常使用 HOC/render props 等方式來復用組件的狀態(tài),增強功能等,無疑增加了組件樹層數(shù)及渲染,而在 React Hooks 中,這些功能都可以通過強大的自定義的 Hooks 來實現(xiàn);
關于這方面的文章,我們根據使用場景分別進行舉例說明,幫助你認識理解并可以熟練運用 React Hooks 大部分特性。辛苦整理良久,還望手動點贊鼓勵~
一、State Hook
1、基礎用法
function?State(){
??const?[count,?setCount]?=?useState(0);
??return?(
??????<div>
??????????<p>You?clicked?{count}?timesp>
??????????<button?onClick={()?=>?setCount(count?+?1)}>
??????????????Click?me
??????????button>
??????div>
??)
}
2、更新
更新分為以下兩種方式,即直接更新和函數(shù)式更新,其應用場景的區(qū)分點在于:
直接更新不依賴于舊 state 的值;函數(shù)式更新依賴于舊 state 的值;
//?直接更新
setState(newCount);
//?函數(shù)式更新
setState(prevCount?=>?prevCount?-?1);
3、實現(xiàn)合并
與 class 組件中的 setState 方法不同,useState 不會自動合并更新對象,而是直接替換它。我們可以用函數(shù)式的 setState 結合展開運算符來達到合并更新對象的效果。
setState(prevState?=>?{
??//?也可以使用?Object.assign
??return?{...prevState,?...updatedValues};
});
4、惰性初始化 state
initialState 參數(shù)只會在組件的初始渲染中起作用,后續(xù)渲染時會被忽略。其應用場景在于:創(chuàng)建初始 state 很昂貴時,例如需要通過復雜計算獲得;那么則可以傳入一個函數(shù),在函數(shù)中計算并返回初始的 state,此函數(shù)只在初始渲染時被調用:
const?[state,?setState]?=?useState(()?=>?{
??const?initialState?=?someExpensiveComputation(props);
??return?initialState;
});
5、一些重點
(1)不像 class 中的 this.setState ,Hook 更新 state 變量總是替換它而不是合并它; (2)推薦使用多個 state 變量,而不是單個 state 變量,因為 state 的替換邏輯而不是合并邏輯,并且利于后續(xù)的相關 state 邏輯抽離; (3)調用 State Hook 的更新函數(shù)并傳入當前的 state 時,React 將跳過子組件的渲染及 effect 的執(zhí)行。(React 使用 Object.is 比較算法 來比較 state。)
二、Effect Hook
1、基礎用法
function?Effect(){
??const?[count,?setCount]?=?useState(0);
??useEffect(()?=>?{
????console.log(`You?clicked?${count}?times`);
??});
??return?(
??????<div>
??????????<p>You?clicked?{count}?timesp>
??????????<button?onClick={()?=>?setCount(count?+?1)}>
??????????????Click?me
??????????button>
??????div>
??)
}
2、清除操作
為防止內存泄漏,清除函數(shù)會在組件卸載前執(zhí)行;如果組件多次渲染(通常如此),則在執(zhí)行下一個 effect 之前,上一個 effect 就已被清除,即先執(zhí)行上一個 effect 中 return 的函數(shù),然后再執(zhí)行本 effect 中非 return 的函數(shù)。
useEffect(()?=>?{
??const?subscription?=?props.source.subscribe();
??return?()?=>?{
????//?清除訂閱
????subscription.unsubscribe();
??};
});
3、執(zhí)行時期
與 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 調度的 effect 不會阻塞瀏覽器更新屏幕,這讓你的應用看起來響應更快;(componentDidMount 或 componentDidUpdate 會阻塞瀏覽器更新屏幕)
4、性能優(yōu)化
默認情況下,React 會每次等待瀏覽器完成畫面渲染之后延遲調用 effect;但是如果某些特定值在兩次重渲染之間沒有發(fā)生變化,你可以通知 React 跳過對 effect 的調用,只要傳遞數(shù)組作為 useEffect 的第二個可選參數(shù)即可:如下所示,如果 count 值兩次渲染之間沒有發(fā)生變化,那么第二次渲染后就會跳過 effect 的調用;
useEffect(()?=>?{
??document.title?=?`You?clicked?${count}?times`;
},?[count]);?//?僅在?count?更改時更新
5、模擬 componentDidMount
如果想只運行一次的 effect(僅在組件掛載和卸載時執(zhí)行),可以傳遞一個空數(shù)組([ ])作為第二個參數(shù),如下所示,原理跟第 4 點性能優(yōu)化講述的一樣;
useEffect(()?=>?{
??.....
},?[]);
6、最佳實踐
要記住 effect 外部的函數(shù)使用了哪些 props 和 state 很難,這也是為什么 通常你會想要在 effect 內部 去聲明它所需要的函數(shù)。
//?bad,不推薦
function?Example({?someProp?})?{
??function?doSomething()?{
????console.log(someProp);
??}
??useEffect(()?=>?{
????doSomething();
??},?[]);?//????這樣不安全(它調用的?`doSomething`?函數(shù)使用了?`someProp`)
}
//?good,推薦
function?Example({?someProp?})?{
??useEffect(()?=>?{
????function?doSomething()?{
??????console.log(someProp);
????}
????doSomething();
??},?[someProp]);?//???安全(我們的?effect?僅用到了?`someProp`)
}
如果處于某些原因你無法把一個函數(shù)移動到 effect 內部,還有一些其他辦法:
你可以嘗試把那個函數(shù)移動到你的組件之外。那樣一來,這個函數(shù)就肯定不會依賴任何 props 或 state,并且也不用出現(xiàn)在依賴列表中了;萬不得已的情況下,你可以 把函數(shù)加入 effect 的依賴但 把它的定義包裹 進 useCallback Hook。這就確保了它不隨渲染而改變,除非它自身的依賴發(fā)生了改變;
推薦啟用 eslint-plugin-react-hooks 中的 exhaustive-deps 規(guī)則,此規(guī)則會在添加錯誤依賴時發(fā)出警告并給出修復建議 ;
// 1、安裝插件
npm?i?eslint-plugin-react-hooks?--save-dev
// 2、eslint 配置
{
??"plugins":?[
????//?...
????"react-hooks"
??],
??"rules":?{
????//?...
????"react-hooks/rules-of-hooks":?"error",
????"react-hooks/exhaustive-deps":?"warn"
??}
}
7、一些重點
(1)可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate和 componentWillUnmount這三個函數(shù)的組合; (2)在 React 的 class 組件中,render 函數(shù)是不應該有任何副作用的;一般來說,在這里執(zhí)行操作太早了,我們基本上都希望在 React 更新 DOM 之后才執(zhí)行我們的操作。
三、useContext
用來處理多層級傳遞數(shù)據的方式,在以前組件樹中,跨層級祖先組件想要給孫子組件傳遞數(shù)據的時候,除了一層層 props 往下透傳之外,我們還可以使用 React Context API 來幫我們做這件事。使用例子如下所示 (1)使用 React Context API,在組件外部建立一個 Context
import?React?from?'react';
const?ThemeContext?=?React.createContext(0);
export?default?ThemeContext;
(2)使用 Context.Provider提供了一個 Context 對象,這個對象可以被子組件共享
import?React,?{?useState?}?from?'react';
import?ThemeContext?from?'./ThemeContext';
import?ContextComponent1?from?'./ContextComponent1';
function?ContextPage?()?{
??const?[count,?setCount]?=?useState(1);
??return?(
????<div?className="App">
??????<ThemeContext.Provider?value={count}>
????????<ContextComponent1?/>
??????ThemeContext.Provider>
??????<button?onClick={()?=>?setCount(count?+?1)}>
??????????????Click?me
??????button>
????div>
??);
}
export?default?ContextPage;
(3)useContext()鉤子函數(shù)用來引入 Context 對象,并且獲取到它的值 // 子組件,在子組件中使用孫組件
import?React?from?'react';
import?ContextComponent2?from?'./ContextComponent2';
function?ContextComponent?()?{
??return?(
????<ContextComponent2?/>
??);
}
export?default?ContextComponent;
// 孫組件,在孫組件中使用 Context 對象值
import?React,?{?useContext?}?from?'react';
import?ThemeContext?from?'./ThemeContext';
function?ContextComponent?()?{
??const?value?=?useContext(ThemeContext);
??return?(
????<div>useContext:{value}div>
??);
}
export?default?ContextComponent;
四、useReducer
1、基礎用法
比 useState 更適用的場景:例如 state 邏輯處理較復雜且包含多個子值,或者下一個 state 依賴于之前的 state 等;例子如下所示
import?React,?{?useReducer?}?from?'react';
interface?stateType?{
??count:?number
}
interface?actionType?{
??type:?string
}
const?initialState?=?{?count:?0?};
const?reducer?=?(state:stateType,?action:actionType)?=>?{
??switch?(action.type)?{
????case?'increment':
??????return?{?count:?state.count?+?1?};
????case?'decrement':
??????return?{?count:?state.count?-?1?};
????default:
??????throw?new?Error();
??}
};
const?UseReducer?=?()?=>?{
??const?[state,?dispatch]?=?useReducer(reducer,?initialState);
??return?(
????<div?className="App">
??????<div>useReducer?Count:{state.count}div>
??????<button?onClick={()?=>?{?dispatch({?type:?'decrement'?});?}}>useReducer?減少button>
??????<button?onClick={()?=>?{?dispatch({?type:?'increment'?});?}}>useReducer?增加button>
????div>
??);
};
export?default?UseReducer;
2、惰性初始化 state
interface?stateType?{
??count:?number
}
interface?actionType?{
??type:?string,
??paylod?:?number
}
const?initCount?=0?
const?init?=?(initCount:number)=>{
??return?{count:initCount}
}
const?reducer?=?(state:stateType,?action:actionType)=>{
??switch(action.type){
????case?'increment':
??????return?{count:?state.count?+?1}
????case?'decrement':
??????return?{count:?state.count?-?1}
????case?'reset':
??????return?init(action.paylod?||?0)
????default:
??????throw?new?Error();
??}
}
const?UseReducer?=?()?=>?{
??const?[state,?dispatch]?=?useReducer(reducer,initCount,init)
??return?(
????<div?className="App">
??????<div>useReducer?Count:{state.count}div>
??????<button?onClick={()=>{dispatch({type:'decrement'})}}>useReducer?減少button>
??????<button?onClick={()=>{dispatch({type:'increment'})}}>useReducer?增加button>
??????<button?onClick={()=>{dispatch({type:'reset',paylod:10?})}}>useReducer?增加button>
????div>
??);
}
export?default?UseReducer;
五、Memo
如下所示,當父組件重新渲染時,子組件也會重新渲染,即使子組件的 props 和 state 都沒有改變
import?React,?{?memo,?useState?}?from?'react';
//?子組件
const?ChildComp?=?()?=>?{
??console.log('ChildComp...');
??return?(<div>ChildComp...div>);
};
//?父組件
const?Parent?=?()?=>?{
??const?[count,?setCount]?=?useState(0);
??return?(
????<div?className="App">
??????<div>hello?world?{count}div>
??????<div?onClick={()?=>?{?setCount(count?=>?count?+?1);?}}>點擊增加div>
??????<ChildComp/>
????div>
??);
};
export?default?Parent;
改進:我們可以使用 memo 包一層,就能解決上面的問題;但是僅僅解決父組件沒有傳參給子組件的情況以及父組件傳簡單類型的參數(shù)給子組件的情況(例如 string、number、boolean等);如果有傳復雜屬性應該使用 useCallback(回調事件)或者 useMemo(復雜屬性)
//?子組件
const?ChildComp?=?()?=>?{
??console.log('ChildComp...');
??return?(<div>ChildComp...div>);
};
const?MemoChildComp?=?memo(ChildComp);
六、useMemo
假設以下場景,父組件在調用子組件時傳遞 info 對象屬性,點擊父組件按鈕時,發(fā)現(xiàn)控制臺會打印出子組件被渲染的信息。
import?React,?{?memo,?useState?}?from?'react';
//?子組件
const?ChildComp?=?(info:{info:{name:?string,?age:?number}})?=>?{
??console.log('ChildComp...');
??return?(<div>ChildComp...div>);
};
const?MemoChildComp?=?memo(ChildComp);
//?父組件
const?Parent?=?()?=>?{
??const?[count,?setCount]?=?useState(0);
??const?[name]?=?useState('jack');
??const?[age]?=?useState(11);
??const?info?=?{?name,?age?};
??return?(
????<div?className="App">
??????<div>hello?world?{count}div>
??????<div?onClick={()?=>?{?setCount(count?=>?count?+?1);?}}>點擊增加div>
??????<MemoChildComp?info={info}/>
????div>
??);
};
export?default?Parent;
分析原因:
點擊父組件按鈕,觸發(fā)父組件重新渲染;父組件渲染,const info = { name, age } 一行會重新生成一個新對象,導致傳遞給子組件的 info 屬性值變化,進而導致子組件重新渲染。
解決:
使用 useMemo 將對象屬性包一層,useMemo 有兩個參數(shù):
第一個參數(shù)是個函數(shù),返回的對象指向同一個引用,不會創(chuàng)建新對象; 第二個參數(shù)是個數(shù)組,只有數(shù)組中的變量改變時,第一個參數(shù)的函數(shù)才會返回一個新的對象。
import?React,?{?memo,?useMemo,?useState?}?from?'react';
//?子組件
const?ChildComp?=?(info:{info:{name:?string,?age:?number}})?=>?{
??console.log('ChildComp...');
??return?(<div>ChildComp...div>);
};
const?MemoChildComp?=?memo(ChildComp);
//?父組件
const?Parent?=?()?=>?{
??const?[count,?setCount]?=?useState(0);
??const?[name]?=?useState('jack');
??const?[age]?=?useState(11);
??
??//?使用?useMemo?將對象屬性包一層
??const?info?=?useMemo(()?=>?({?name,?age?}),?[name,?age]);
??return?(
????<div?className="App">
??????<div>hello?world?{count}div>
??????<div?onClick={()?=>?{?setCount(count?=>?count?+?1);?}}>點擊增加div>
??????<MemoChildComp?info={info}/>
????div>
??);
};
export?default?Parent;
七 、useCallback
接著第六章節(jié)的例子,假設需要將事件傳給子組件,如下所示,當點擊父組件按鈕時,發(fā)現(xiàn)控制臺會打印出子組件被渲染的信息,說明子組件又被重新渲染了。
import?React,?{?memo,?useMemo,?useState?}?from?'react';
//?子組件
const?ChildComp?=?(props:any)?=>?{
??console.log('ChildComp...');
??return?(<div>ChildComp...div>);
};
const?MemoChildComp?=?memo(ChildComp);
//?父組件
const?Parent?=?()?=>?{
??const?[count,?setCount]?=?useState(0);
??const?[name]?=?useState('jack');
??const?[age]?=?useState(11);
??const?info?=?useMemo(()?=>?({?name,?age?}),?[name,?age]);
??const?changeName?=?()?=>?{
????console.log('輸出名稱...');
??};
??return?(
????<div?className="App">
??????<div>hello?world?{count}div>
??????<div?onClick={()?=>?{?setCount(count?=>?count?+?1);?}}>點擊增加div>
??????<MemoChildComp?info={info}?changeName={changeName}/>
????div>
??);
};
export?default?Parent;
分析下原因:
點擊父組件按鈕,改變了父組件中 count 變量值(父組件的 state 值),進而導致父組件重新渲染;父組件重新渲染時,會重新創(chuàng)建 changeName 函數(shù),即傳給子組件的 changeName 屬性發(fā)生了變化,導致子組件渲染;
解決:
修改父組件的 changeName 方法,用 useCallback 鉤子函數(shù)包裹一層, useCallback 參數(shù)與 useMemo 類似
import?React,?{?memo,?useCallback,?useMemo,?useState?}?from?'react';
//?子組件
const?ChildComp?=?(props:any)?=>?{
??console.log('ChildComp...');
??return?(<div>ChildComp...div>);
};
const?MemoChildComp?=?memo(ChildComp);
//?父組件
const?Parent?=?()?=>?{
??const?[count,?setCount]?=?useState(0);
??const?[name]?=?useState('jack');
??const?[age]?=?useState(11);
??const?info?=?useMemo(()?=>?({?name,?age?}),?[name,?age]);
??const?changeName?=?useCallback(()?=>?{
????console.log('輸出名稱...');
??},?[]);
??return?(
????<div?className="App">
??????<div>hello?world?{count}div>
??????<div?onClick={()?=>?{?setCount(count?=>?count?+?1);?}}>點擊增加div>
??????<MemoChildComp?info={info}?changeName={changeName}/>
????div>
??);
};
export?default?Parent;
八、useRef
以下分別介紹 useRef 的兩個使用場景:
1、指向 dom 元素
如下所示,使用 useRef 創(chuàng)建的變量指向一個 input 元素,并在頁面渲染后使 input 聚焦
import?React,?{?useRef,?useEffect?}?from?'react';
const?Page1?=?()?=>?{
??const?myRef?=?useRef(null);
??useEffect(()?=>?{
????myRef?.current?.focus();
??});
??return?(
????<div>
??????<span>UseRef:span>
??????<input?ref={myRef}?type="text"/>
????div>
??);
};
export?default?Page1;
2、存放變量
useRef 在 react hook 中的作用, 正如官網說的, 它像一個變量, 類似于 this , 它就像一個盒子, 你可以存放任何東西. createRef 每次渲染都會返回一個新的引用,而 useRef 每次都會返回相同的引用,如下例子所示:
import?React,?{?useRef,?useEffect,?useState?}?from?'react';
const?Page1?=?()?=>?{
????const?myRef2?=?useRef(0);
????const?[count,?setCount]?=?useState(0)
????useEffect(()=>{
??????myRef2.current?=?count;
????});
????function?handleClick(){
??????setTimeout(()=>{
????????console.log(count);?//?3
????????console.log(myRef2.current);?//?6
??????},3000)
????}
????return?(
????<div>
??????<div?onClick={()=>?setCount(count+1)}>點擊countdiv>
??????<div?onClick={()=>?handleClick()}>查看div>
????div>
????);
}
export?default?Page1;
九、useImperativeHandle
使用場景:通過 ref 獲取到的是整個 dom 節(jié)點,通過 useImperativeHandle 可以控制只暴露一部分方法和屬性,而不是整個 dom 節(jié)點。
十、useLayoutEffect
其函數(shù)簽名與 useEffect 相同,但它會在所有的 DOM 變更之后同步調用 effect,這里不再舉例。
useLayoutEffect 和平常寫的 Class 組件的 componentDidMount 和 componentDidUpdate 同時執(zhí)行;
useEffect 會在本次更新完成后,也就是第 1 點的方法執(zhí)行完成后,再開啟一次任務調度,在下次任務調度中執(zhí)行 useEffect;
總結
關于這方面的文章,我們根據使用場景分別進行舉例說明,希望有幫助到你認識理解并可以熟練運用 React Hooks 大部分特性。
辛苦整理良久,還望手動點贊鼓勵~
原文:https://juejin.cn/post/6918896729366462471
??愛心三連擊 1.看到這里了就點個在看支持下吧,你的「點贊,在看」是我創(chuàng)作的動力。
2.關注公眾號
程序員成長指北,回復「1」加入高級前端交流群!「在這里有好多 前端?開發(fā)者,會討論?前端 Node 知識,互相學習」!3.也可添加微信【ikoala520】,一起成長。
“在看轉發(fā)”是最大的支持
