React中的Refs為我們提供了一種在組件的整個(gè)生命周期中存儲(chǔ)可變值的方法,并且通常用于與DOM交互而無(wú)需重新渲染組件。換句話說(shuō),我們不需要依賴狀態(tài)管理來(lái)使用Refs更新元素。這在某些特定的使用案例中非常有用,但在代替狀態(tài)管理或生命周期方法集成時(shí)班眯,也被視為一種反模式。
hooks已經(jīng)集成到react的框架中烁巫,使用生命周期的類組件署隘,現(xiàn)在可以替換成為函數(shù)組件和hooks。
該useRefhook已實(shí)現(xiàn)為在函數(shù)組件中使用React ref的解決方案程拭。在本文中定踱,我們將與其他可以一起工作的鉤子一起探索這個(gè)鉤子。更具體地說(shuō)恃鞋,我們將:
- 介紹如何將
useRef
hook與函數(shù)組件結(jié)合使用崖媚,并介紹與useEffect
和共同使用的hook的一些用例useLayoutEffect - 如何
useRef
與并發(fā)React一起正確使用 - 探索
useRef
演示組件的用例
這里我們主要關(guān)注function組件,盡管這絕不意味著它們應(yīng)該在類組件上使用恤浪,這也帶來(lái)了它們的好處畅哑。
將項(xiàng)目移至函數(shù)組件是否值得?
函數(shù)組件提供速度和樣板優(yōu)化的地方水由,類組件提供了久經(jīng)考驗(yàn)的組件生命周期方法和眾所周知的結(jié)構(gòu)荠呐,我們都已經(jīng)習(xí)慣了(使用this
類屬性等)。
為了促進(jìn)代碼的一致性砂客,開(kāi)發(fā)人員通常選擇對(duì)項(xiàng)目使用純類組件方法或純函數(shù)組件方法泥张。我個(gè)人選擇創(chuàng)建函數(shù)組件作為我的默認(rèn)選擇,并退回到有意義的類組件鞠值,例如當(dāng)我想顯式定義生命周期方法媚创,許多類屬性等時(shí)。
以我的經(jīng)驗(yàn)彤恶,Typescript似乎比功能性組件更多地補(bǔ)充了類組件钞钙,能夠?qū)㈩愋鸵约皩傩院头椒ú迦腩愋捅旧眦佟n惥哂懈敿?xì)的結(jié)構(gòu)。
在任何情況下芒炼,React Ref都可以在類和函數(shù)組件中使用瘫怜。讓我們探討一下如何使用帶有useRef
鉤子的引用。
使用 useRef Hook
函數(shù)組件Ref的實(shí)現(xiàn)已通過(guò)名為的鉤子實(shí)現(xiàn)useRef本刽。讓我們看看它是如何集成的鲸湃,然后探究它的特性以及何時(shí)使用它。
Ref可以在組件內(nèi)定義:
import React, { useRef } from 'react';
...
const refContainer = useRef(initialValue);
這個(gè)鉤子有一個(gè)非常簡(jiǎn)單的API-可以說(shuō)比它的類更簡(jiǎn)單盅安。選擇該refContainer
名稱(來(lái)自官方文檔)以反映該變量實(shí)際上充當(dāng)基礎(chǔ)引用的容器唤锉。
為了與Refs的基類實(shí)現(xiàn)一致,被引用的對(duì)象本身存儲(chǔ)在current
此容器變量的屬性中别瞭。有關(guān)此current
屬性的兩個(gè)關(guān)鍵事實(shí):
- 該屬性是可變的
- 它可以在組件生命周期中隨時(shí)更改
函數(shù)組件仍然具有類組件的生命周期,盡管沒(méi)有生命周期方法株憾。組件生命周期的考慮將變得越來(lái)越重要蝙寨。
另外,initialValue我們上面?zhèn)魅氲膮?shù)可用于使用current默認(rèn)值進(jìn)行初始化嗤瞎。該值通常充當(dāng)占位符墙歪,直到我們實(shí)際引用DOM中的元素或?yàn)槠浞峙淙我庵禐橹埂?/p>
事件盡管Ref通常用于引用DOM元素,但它們也可以存儲(chǔ)原始類型和對(duì)象贝奇。我們將介紹這兩種情況的示例虹菲。
傳入的初始值也是完全合法的null:
//初始化一個(gè)空引用
const myRef = useRef(null);
無(wú)論current是什么,我們都可以在組件生命周期的任何時(shí)間log該屬性以查看其值:
//親自查看Ref實(shí)際引用的是什么
console.log(refContainer.current);
通過(guò)ref屬性完成對(duì)DOM元素的引用掉瞳。我們r(jià)eturn通過(guò)ref屬性在語(yǔ)句內(nèi)的JSX級(jí)別上執(zhí)行此操作毕源。下面使用button元素完全做到了這一點(diǎn):
// referencing a `button` element
...
render() {
return(
<button ref={refContainer}>
Press Me
</button>
);
}
記住,我們引用的是DOM HTML元素陕习,而不是React組件霎褐。
如果引用的是按鈕,則將refContainer.current指向該<button />DOM元素该镣,從而使我們能夠訪問(wèn)諸如使按鈕聚焦/模糊的控件以及其樣式和事件處理程序(onClick例如冻璃,觸發(fā)click事件)。
模糊是用于使活動(dòng)元素處于非活動(dòng)狀態(tài)的術(shù)語(yǔ)损合。如果一個(gè)文本框處于活動(dòng)狀態(tài)(光標(biāo)在內(nèi)部閃爍省艳,準(zhǔn)備輸入文本),則可以通過(guò)單擊該元素外部的來(lái)模糊該元素以取消選擇它嫁审,或者通過(guò)編程使用諸如Refs之類的功能跋炕。
用refs管理按鈕狀態(tài)
讓我們來(lái)看一下useRef按鈕的第一個(gè)有效用例。
按鈕是一個(gè)很好的用例土居,可以與useRef按鈕一起使用枣购,從而可以控制按鈕的狀態(tài)(不要與組件狀態(tài)混淆)嬉探,而無(wú)需重新渲染整個(gè)組件。
讓我們考慮一個(gè)真實(shí)的場(chǎng)景棉圈。也許表單已經(jīng)完成涩堤,并且需要從默認(rèn)的禁用狀態(tài)啟用提交按鈕。僅為了執(zhí)行此操作而重新渲染我的整個(gè)表單將需要我:
- 將所有當(dāng)前表單值保存到狀態(tài)
- 使用這些當(dāng)前值再次重新渲染整個(gè)表單
- 保持子組件中可能存在的任何其他狀態(tài)分瘾,例如驗(yàn)證消息和可視指示器
- 重置可能發(fā)生的所有過(guò)渡或動(dòng)畫(huà)
React要在后臺(tái)處理很多工作胎围。只需引用DOM中的按鈕以切換其disabled屬性,在這里就更有意義了:
refContainer.current.setAttribute("disabled", true);
// or
refContainer.current.removeAttribute("disabled");
到此為止德召,我們現(xiàn)在已經(jīng)介紹了的基本APIuseRef
以及可行的用例-現(xiàn)在讓我們看看如何useRef
與useEffecthook一起正確實(shí)現(xiàn)白魂。
在Commit組件階段正確實(shí)現(xiàn)useRef
為了完全理解如何實(shí)現(xiàn)useRef,我們需要了解React組件執(zhí)行的兩個(gè)階段上岗,以及這與React ref的工作如何聯(lián)系福荸。
可以在函數(shù)組件的主塊中定義Ref,但是在確定了將在Component中更新的內(nèi)容之后肴掷,必須在組件的commit階段中定義與Ref相關(guān)的任何副作用敬锐,例如事件偵聽(tīng)器或計(jì)時(shí)器。DOM發(fā)生呆瞻。讓我們更詳細(xì)地訪問(wèn)它台夺。
渲染與提交階段
一個(gè)組件經(jīng)歷兩個(gè)高級(jí)階段:
- 所述渲染階段確定從先前的對(duì)DOM做出呈現(xiàn)的變化,并調(diào)用的方法痴脾,如componentWillMount颤介,render和setState(主要)
- 在
commit
(提交)階段,顧名思義赞赖,提交更改(即呈現(xiàn)階段確定)的DOM滚朵,并調(diào)用方法,包括componentDidMount薯定,componentDidUpdate始绍,和componentDidCatch
如果要實(shí)現(xiàn)引用,則第一階段渲染具有我們需要關(guān)注的關(guān)鍵特征:在執(zhí)行提交階段之前可能會(huì)多次調(diào)用它–這是有問(wèn)題的话侄,在我們的應(yīng)用程序中引入了不可預(yù)測(cè)性和錯(cuò)誤的可能性亏推。
引用應(yīng)在提交階段實(shí)現(xiàn)
另一方面,提交階段只能調(diào)用一次年堆,這是我們應(yīng)該定義副作用的階段吞杭,通常來(lái)說(shuō),我們只希望實(shí)例化一次变丧。
副作用是任何會(huì)影響正在執(zhí)行的函數(shù)范圍之外的內(nèi)容的東西芽狗。這些可以是API請(qǐng)求,網(wǎng)絡(luò)套接字痒蓬,計(jì)時(shí)器童擎,記錄器滴劲,甚至是引用中的任何內(nèi)容。
如果某個(gè)組件重新渲染多次并在該階段重新初始化Ref顾复,則該Ref邏輯將執(zhí)行相同的次數(shù)班挖。當(dāng)我們考慮React中的并發(fā)模式時(shí),這更令人擔(dān)憂芯砸,因?yàn)榻M件的渲染階段可以在初始渲染上執(zhí)行多次萧芙。
這是因?yàn)椴l(fā)模式將渲染過(guò)程分解為多個(gè)部分,經(jīng)常在需要執(zhí)行其他更高優(yōu)先級(jí)的異步過(guò)程時(shí)暫停和恢復(fù)工作假丧。這樣做的結(jié)果是有可能在提交之前多次調(diào)用渲染階段生命周期方法(如果有錯(cuò)誤双揪,則根本不調(diào)用)。
定義副作用或超出組件范圍的任何內(nèi)容都是不可靠的包帚。我們可以做很多事情來(lái)確保在開(kāi)發(fā)時(shí)不會(huì)落入這些陷阱渔期。
1.使用嚴(yán)格模式
我們可以做的第一件事就是實(shí)現(xiàn)嚴(yán)格模式。嚴(yán)格模式旨在通過(guò)控制臺(tái)突出顯示應(yīng)用程序編碼的各種問(wèn)題渴邦,并有完整的章節(jié)專門(mén)用于檢測(cè)意外的副作用擎场。
可以通過(guò)JXS在整個(gè)應(yīng)用程序中啟用嚴(yán)格模式,也可以僅在一定數(shù)量的子組件上啟用嚴(yán)格模式几莽。包裝整個(gè)應(yīng)用程序以在整個(gè)過(guò)程中應(yīng)用嚴(yán)格模式:
// wrapping your app in Strict Mode
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>
, document.getElementById('root'));
與一樣React.Fragment,嚴(yán)格模式不會(huì)在您的應(yīng)用中產(chǎn)生任何其他標(biāo)記宅静。
這是我偏愛(ài)的方法章蚣,可確保正確設(shè)計(jì)函數(shù)組件的邏輯,但還有其他選擇可用姨夹。
2.在并發(fā)模式下測(cè)試您的應(yīng)用
一種更直接的方法是在并發(fā)模式下開(kāi)發(fā)應(yīng)用程序纤垂,這還將在開(kāi)發(fā)過(guò)程中標(biāo)記問(wèn)題。
有兩種啟用并發(fā)模式的方法磷账,要么包裝元素的子集峭沦,要么使用并發(fā)模式聲明包裝整個(gè)應(yīng)用程序:
// section of an app (not final API)
<React.unstable_ConcurrentMode>
<MyComponent />
</React.unstable_ConcurrentMode>
// entire app (not final API)
ReactDOM.unstable_createRoot(domNode).render(<App />);
這些不是最終的API,但是為開(kāi)發(fā)人員提供了至少一些測(cè)試React應(yīng)用程序異步版本的方法逃糟。該博客文章實(shí)際上建議使用嚴(yán)格模式作為準(zhǔn)備應(yīng)用程序并發(fā)的主要方法吼鱼。
由于進(jìn)行中的工作性質(zhì)不穩(wěn)定,建議不要在最終版本之前部署啟用并發(fā)模式的應(yīng)用程序的生產(chǎn)版本绰咽。
考慮到以上幾點(diǎn)菇肃,現(xiàn)在讓我們useRef在提交階段檢查一下實(shí)際完成的實(shí)現(xiàn)。
使用useEffect實(shí)現(xiàn)useRef
避免我們一直在討論的不可預(yù)測(cè)的Ref行為的解決方案是在useEffect or useLayoutEffect鉤子內(nèi)部實(shí)現(xiàn)Ref副作用取募。
為什么是這樣琐谤?因?yàn)楦鶕?jù)官方文檔,函數(shù)組件的主體內(nèi)部不允許出現(xiàn)諸如變異玩敏,訂閱斗忌,計(jì)時(shí)器和日志記錄之類的副作用质礼。該主體內(nèi)部的所有邏輯都在渲染階段執(zhí)行,因此導(dǎo)致UI中令人困惑的錯(cuò)誤和不一致织阳。
useEffect
另一方面眶蕉,在瀏覽器中更新了實(shí)際的DOM后,它將運(yùn)行一次陈哑。useEffect
因此將在組件的提交階段運(yùn)行妻坝。
可以創(chuàng)建一個(gè)簡(jiǎn)單的計(jì)數(shù)器組件來(lái)演示這一點(diǎn),每次重新渲染組件時(shí)我們都在其中進(jìn)行計(jì)數(shù)惊窖。為了在單擊按鈕時(shí)強(qiáng)制重新渲染組件刽宪,useReducer還實(shí)現(xiàn)了該hook:
import React, { useEffect, useReducer, useRef } from "react";
const useForceRerender = () => useReducer(state => !state, false)[1];
const Counter = () => {
const forceRerender = useForceRerender();
const refCount = useRef(0);
useEffect(() => {
refCount.current += 1;
});
return (
<>
<p>Count: {refCount.current}</p>
<p>
<button onClick={forceRerender}>
Increment Counter
</button>
</p>
</>;
);
};
export default Counter;
在上面的示例中,refCount.current從值開(kāi)始0界酒,并在組件更新的提交階段遞增圣拄。
請(qǐng)記住,如果我們要在主功能塊中進(jìn)行此增量毁欣,則更新將在返回render函數(shù)之前在render階段進(jìn)行庇谆,使增量遭受不可預(yù)測(cè)的重復(fù)。
現(xiàn)在凭疮,讓我們看一下另一個(gè)引用DOM元素的組件饭耳,并通過(guò)其ref將事件偵聽(tīng)器附加到該組件。事件偵聽(tīng)器再次在useEffecthook中定義执解。此外寞肖,useEffect當(dāng)組件被卸載時(shí),act的返回函數(shù)可作為整齊的手段觸發(fā)衰腌,在這里我們可以從ref中刪除事件監(jiān)聽(tīng)器:
import React, { useEffect, useRef } from 'react';
function App () {
const refInput = useRef();
useEffect(() => {
const { current } = refInput;
const handleFocus = () => {
console.log('input is focussed');
}
const handleBlur = () => {
console.log('input is blurred');
}
current.addEventListener('focus', handleFocus);
current.addEventListener('blur', handleBlur);
return () => {
current.removeEventListener('focus', handleFocus);
current.removeEventListener('blur', handleBlur);
}
});
return (
<p>
<input
type="text"
ref={refInput}
defaultValue="Focus me"
/>
</p>
);
}
export default App;
現(xiàn)在新蟆,如果您單擊文本輸入,然后單擊相應(yīng)的console.log輸出右蕊,則相應(yīng)的輸出將通知您該文本輸入正在聚焦和模糊琼稻。
讓我們進(jìn)一步了解這個(gè)概念。下一個(gè)示例通過(guò)refInputRef操作按鈕元素的類饶囚。我們還引入了<Wrapper />樣式化組件來(lái)定義一個(gè)active類帕翻,該類會(huì)更改文本輸入邊框和文本顏色:
import React, { useEffect, useRef } from 'react';
import styled from 'styled-components';
const Wrapper = styled.div`
input {
color: #666;
border: 1px solid #ccc;
outline: none;
&.active {
color: #000;
border-color: #000;
}
}
`;
function App () {
const refInput = useRef();
useEffect(() => {
const { current } = refInput;
const handleFocus = () => {
current.classList.add('active');
}
const handleBlur = () => {
current.classList.remove('active');
}
current.addEventListener('focus', handleFocus);
current.addEventListener('blur', handleBlur);
return () => {
current.removeEventListener('focus', handleFocus);
current.removeEventListener('blur', handleBlur);
}
});
return (
<Wrapper>
<input
type="text"
ref={refInput}
defaultValue="Focus me"
/>
</Wrapper>
);
}
export default App;
現(xiàn)在,我們無(wú)需依賴狀態(tài)更新就可以進(jìn)行一些CSS操作坯约。
演示的最后階段是實(shí)現(xiàn)我們之前討論的內(nèi)容—實(shí)現(xiàn)一個(gè)Submit按鈕熊咽,如果文本輸入的值為空,則禁用它闹丐。為此横殴,我useRef為提交按鈕本身引入了一個(gè)額外的鉤子。為了切換disabled屬性,RefsetAttribute和removeAttributeJavascript API已被使用refSubmit衫仑。
完整的解決方案如下:
import React, { useEffect, useRef } from 'react';
import styled from 'styled-components';
const Wrapper = styled.div`
.text {
color: #666;
border: 1px solid #ccc;
outline: none;
&.active {
color: #000;
border-color: #000;
}
}
`;
function App () {
const refInput = useRef();
const refSubmit = useRef();
useEffect(() => {
const { current } = refInput;
const handleFocus = () => {
current.classList.add('active');
}
const handleBlur = () => {
current.classList.remove('active');
current.value !== ''
? refSubmit.current.removeAttribute('disabled')
: refSubmit.current.setAttribute('disabled', true);
}
current.addEventListener('focus', handleFocus);
current.addEventListener('blur', handleBlur);
return () => {
current.removeEventListener('focus', handleFocus);
current.removeEventListener('blur', handleBlur);
}
});
return (
<Wrapper>
<p>
<input
className='text'
type="text"
ref={refInput}
defaultValue="Focus me"
/>
</p>
<p>
<input
ref={refSubmit}
type="submit"
value="Submit"
/>
</p>
</Wrapper>
);
}
export default App;
disabled一旦我們?cè)谖谋据斎胫鈫螕簦ɑ螯c(diǎn)擊)梨与,或者當(dāng)它變得模糊時(shí),提交按鈕的屬性就會(huì)更新文狱,這是確定表單是否有效的自然時(shí)間粥鞋。
one more thing
本文的最后一部分將介紹值得注意的使用技巧useRef。
當(dāng)useEffect引起問(wèn)題時(shí)瞄崇,請(qǐng)使用useLayoutEffect
在本文開(kāi)頭呻粹,我們提到了該useLayoutEffect
鉤子,該鉤子也常與一起使用useRef
苏研。useLayoutEffect
與componentDidMount
和componentDidUpdate
類組件生命周期方法在同一階段觸發(fā)等浊,因此您可能傾向于使用它代替useEffect
。
但是摹蘑,官方文檔建議開(kāi)發(fā)人員應(yīng)useEffect
主要嘗試使用筹燕,useLayoutEffect
如果出現(xiàn)問(wèn)題請(qǐng)退后。使用會(huì)犧牲一些速度useLayoutEffect
衅鹿,因?yàn)橹挥性谒蠨OM突變/更新都發(fā)生后才被同步調(diào)用-除了這個(gè)細(xì)節(jié)撒踪,它與相同useEffect
。
轉(zhuǎn)發(fā)useRef的
就像在類組件中初始化的轉(zhuǎn)發(fā)Refs一樣useRef
大渤,只要遵循相同的約定制妄,也可以轉(zhuǎn)發(fā)通過(guò)鉤子初始化的Refs 。不要將ref作為“ ref
”屬性傳遞-這是React中保留的屬性名稱泵三,會(huì)導(dǎo)致錯(cuò)誤忍捡。相反,一個(gè)名為的道具forwardRef
切黔。
...
// defining `refInput` within `App`, forwarding it to `MyInput`
function App () {
const refInput = useRef();
return <MyInput
forwardRef={refInput};
}
// referencing `input` element with `forwardRef` in child component
function MyInput (props) {
// verifying `input` is referenced correctly after DOM updates
useLayoutEffect(() => {
console.log(props.forwardRef.current);
});
const { forwardRef } = props;
return (
<input
ref={forwardRef}
type="submit"
value="Submit"
/>);
}
用useRef切換焦點(diǎn)
除了監(jiān)聽(tīng)事件,我們還可以觸發(fā)事件具篇。如果我們不展示此演示纬霞,則演示將不完整。在下面的組件中驱显,單擊一個(gè)按鈕將再次關(guān)注另一個(gè)文本輸入诗芜,再次使用useRef:
// focussing an element with a button press
function TextInput () {
const refInput = useRef();
function handleFocus () {
refInput.current.focus();
}
return (
<>
<input ref={refInput} placeholder="Input Here..." />
<button onClick={handleFocus}>Focus Input</button>
</>
);
}
以編程方式聚焦的元素可以改善用戶體驗(yàn),例如在第一次加載表單并自動(dòng)聚焦第一輸入時(shí)埃疫。
總結(jié)
本文是的useRef總結(jié)伏恐,并介紹了如何在考慮組件生命周期的情況下正確實(shí)現(xiàn)引用。對(duì)于這里討論的用例栓霜,使用refs可能是一種便捷的方法:
- 使用焦點(diǎn)翠桦,模糊,禁用和其他與表單管理相關(guān)的屬性來(lái)微管理輸入
- 要從元素中添加或刪除類,可能控制過(guò)渡或關(guān)鍵幀動(dòng)畫(huà)
- 官方文檔中推薦的ref的另一個(gè)用例是與其他HTML5庫(kù)(例如媒體播放器)進(jìn)行交互销凑。這樣的庫(kù)將無(wú)法通過(guò)React狀態(tài)訪問(wèn)丛晌,而Refs為我們提供了一個(gè)后備功能,可以在與組件的生命周期一致的同時(shí)直接與其他元素進(jìn)行交互