useRef使用細(xì)節(jié)
作者:普拉斯強(qiáng)? ?
來(lái)源:SegmentFault 思否社區(qū)?
一、動(dòng)機(jī)
函數(shù)組件訪問DOM元素;
函數(shù)組件訪問之前渲染變量。
函數(shù)組件每次渲染都會(huì)被執(zhí)行,函數(shù)內(nèi)部的局部變量一般會(huì)重新創(chuàng)建,利用useRef可以訪問上次渲染的變量,類似類組件的實(shí)例變量效果。
1.2 函數(shù)組件使用createRef不行嗎?
createRef主要解決class組件訪問DOM元素問題,并且最佳實(shí)踐是在組件周期內(nèi)只創(chuàng)建一次(一般在構(gòu)造函數(shù)里調(diào)用)。如果在函數(shù)組件內(nèi)使用createRef會(huì)造成每次render都會(huì)調(diào)用createRef:
function?WithCreateRef()?{
??const?[minus,?setMinus]?=?useState(0);
??//?每次render都會(huì)重新創(chuàng)建`ref`
??const?ref?=?React.createRef(null);
??const?handleClick?=?()?=>?{
????setMinus(minus?+?1);
??};
??//?這里每次都是`null`
??console.log(`ref.current=${ref.current}`)
??useEffect(()?=>?{
????console.log(`denp[minus]>`,?ref.current?&&?ref.current.innerText);
??},?[minus]);
??return?(
????"App">
??????Num:?{minus}
??????
????
??);
}
二、使用
2.1 基本語(yǔ)法
見鏈接:https://reactjs.org/docs/hooks-reference.html#useref
每次渲染useRef返回值都不變;
ref.current發(fā)生變化并不會(huì)造成re-render;
ref.current發(fā)生變化應(yīng)該作為Side Effect(因?yàn)樗鼤?huì)影響下次渲染),所以不應(yīng)該在render階段更新current屬性。
2.2?不可以在render里更新ref.current值
在Is there something like instance variables提到:
Unless you’re doing lazy initialization, avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects.
在render里更新refs導(dǎo)致什么問題呢?
在異步渲染里render階段可能會(huì)多次執(zhí)行。
const?RenderCounter?=?()?=>?{
??const?counter?=?useRef(0);
??
??//?counter.current的值可能增加不止一次
??counter.current?=?counter.current?+?1;
??
??return?(
????{`The?component?has?been?re-rendered?${counter.current}?times`}
??);
}
2.3?可以在render里更新ref.current值
同樣也是在Is there something like instance variables提到的:
Unless you’re doing lazy initialization, avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects.
為啥lazy initialization卻可以在render里更新ref.current值?
這個(gè)跟useRef懶初始化的實(shí)現(xiàn)方案有關(guān)。
const?instance?=?React.useRef(null)
if?(instance.current?==?null)?{
??instance.current?=?{
????//?whatever?you?need
??}
}
本質(zhì)上只要保證每次render不會(huì)造成意外效果,都可以在render階段更新ref.current。但最好別這樣,容易造成問題,useRef懶初始化畢竟是個(gè)特殊的例外。
2.4ref.current不可以作為其他hooks(useMemo,?useCallback,?useEffect)依賴項(xiàng)
ref.current的值發(fā)生變更并不會(huì)造成re-render, Reactjs并不會(huì)跟蹤ref.current的變化。
function?Minus()?{
??const?[minus,?setMinus]?=?useState(0);
??const?ref?=?useRef(null);
??const?handleClick?=?()?=>?{
????setMinus(minus?+?1);
??};
??console.log(`ref.current=${ref.current?&&?ref.current.innerText}`)
??//?#1?uesEffect
??useEffect(()?=>?{
????console.log(`denp[ref.current]?>`,?ref.current?&&?ref.current.innerText);
??},?[ref.current]);
??//?#2?uesEffect
??useEffect(()?=>?{
????console.log(`denp[minus]>`,?ref.current?&&?ref.current.innerText);
??},?[minus]);
??return?(
????"App">
??????Num:?{minus}
??????
????
??);
}
本例子中當(dāng)點(diǎn)擊[Add]按鈕兩次后#1 uesEffect就不會(huì)再執(zhí)行了,如圖:

原因分析:
依賴項(xiàng)判斷是在render階段判斷的,發(fā)生在在ref.current更新之前,而useEffect的effect函數(shù)執(zhí)行在渲染之后。
1、第一次執(zhí)行:
首次無(wú)腦執(zhí)行,所以輸出:
ref.current=null
denp[ref.current]?>?Num:?0
denp[minus]>?Num:?0并且此時(shí)ref.current為null,所以?#1 uesEffect相當(dāng)于useEffect(() => console.log('num 1'), [null])
2、點(diǎn)擊[Add],第二次執(zhí)行:
此時(shí)ref.current值為Num: 0
,所以?#1 uesEffect的依賴項(xiàng)發(fā)生變化,最終輸出:
ref.current=Num:?0
denp[ref.current]?>?Num:?1
denp[minus]>?Num:?1
Num: 0])
3、點(diǎn)擊[Add],第三次執(zhí)行:
此時(shí)ref.current值為
Num: 1,所以?#1 uesEffect的依賴項(xiàng)沒有發(fā)生變化,故?#1 uesEffect的effect函數(shù)不會(huì)被執(zhí)行,最終輸出:
ref.current=Num:?1
denp[minus]>?Num:?2
ref.current=Num:?1
denp[minus]>?Num:?2
如果將ref.current作為依賴項(xiàng),eslint-plugin-react-hooks也會(huì)報(bào)警提示的:
React Hook useEffect has an unnecessary dependency: 'ref.current'. Either exclude it or remove the dependency array. Mutable values like 'ref.current' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
2.5?ref作為其他hooks(useMemo,?useCallback,?useEffect)依賴項(xiàng)
ref是不變的,沒必要作為其他hooks依賴。
三、原理

本質(zhì)上是記憶hook,但也可作為data hook,可以簡(jiǎn)單的用useState模擬useRef:
const?useRef?=?(initialValue)?=>?{
??const?[ref]?=?useState({?current:?initialValue});
??return?ref
}點(diǎn)擊左下角閱讀原文,到?SegmentFault 思否社區(qū)?和文章作者展開更多互動(dòng)和交流。
-?END -

