React八大常見(jiàn)錯(cuò)誤及其解決方案
我們會(huì)深度學(xué)習(xí)這 8 大錯(cuò)誤消息,包括但不限于:
- 警告:列表中的每個(gè)子元素都應(yīng)該有一個(gè)唯一的
key
屬性 - 防止在
key
中使用數(shù)組索引 - React Hook
useXXX
的條件調(diào)用。React Hooks 必須在每個(gè)組件渲染中以完全相同的順序調(diào)用 - React Hook 缺少依賴(lài):“XXX”。包含它或刪除依賴(lài)數(shù)組
- 無(wú)法對(duì)已卸載的組件執(zhí)行 React 狀態(tài)更新
- 重新渲染次數(shù)過(guò)多。React 限制渲染次數(shù),防止無(wú)限循環(huán)
- 對(duì)象作為 React 子元素?zé)o效/函數(shù)作為 React 子元素?zé)o效
- 相鄰的 JSX 元素必須包含在封閉標(biāo)簽中
警告:列表中的每個(gè)子元素都應(yīng)該有一個(gè)唯一的 key 屬性
假設(shè)我們有一個(gè)卡片列表,如下所示:
import { Card } from './Card' const data = [ { id: 1, text: '關(guān)注' }, { id: 2, text: '點(diǎn)贊' }, { id: 3, text: '收藏' } ] export default function App() { return ( <div className="container"> {data.map(content => ( <div className="card"> <Card text={content.text} /> </div> ))} </div> ) }
React 開(kāi)發(fā)中最常見(jiàn)的事情之一就是,獲取數(shù)組元素,并使用組件根據(jù)元素內(nèi)容渲染它們。得益于 JSX,我們可以使用 Array.map()
函數(shù)輕松將循環(huán)邏輯嵌入到組件中,并從回調(diào)中返回所需的組件。
雖然但是,在瀏覽器控制臺(tái)中收到 React 警告也司空見(jiàn)慣,提示列表中的每個(gè)子元素都應(yīng)該有一個(gè)唯一的 key
屬性。在養(yǎng)成給每個(gè)子元素一個(gè)獨(dú)特的 key
屬性的習(xí)慣之前,我們可能會(huì)多次遭遇此警告,尤其是如果我們對(duì) React 經(jīng)驗(yàn)較少。但在養(yǎng)成習(xí)慣之前該如何解決呢?
正如警告所示,我們必須將 key
屬性添加到從 map
回調(diào)返回的 JSX 的最外層元素中。雖然但是,我們要使用的 key
有若干要求。key
應(yīng)該是:
- 字符串或數(shù)字
- 對(duì)于列表中的特定元素而言是唯一的
- 跨渲染列表中該元素的代表
export default function App() { return ( <div className="container"> {data.map(content => ( <div key={content.id} className="card"> <Card text={content.text} /> </div> ))} </div> ) }
雖然如果不遵守這些要求,我們的 App 也不會(huì)崩潰,但它可能會(huì)導(dǎo)致某些意外且通常不需要的行為。React 使用這些 key
來(lái)確定列表中的哪些子元素已更改,并使用該信息來(lái)確定可以重用先前 DOM 的哪些部分,以及重新渲染組件時(shí)應(yīng)該重新計(jì)算哪些部分。因此,我們始終建議添加這些 key
。
防止在 key 中使用數(shù)組索引
在上文警告的基礎(chǔ)上,我們將深入學(xué)習(xí)有關(guān) key
的同樣常見(jiàn)的 ESLint 警告。當(dāng)我們養(yǎng)成了在列表中生成的 JSX 中包含 key
屬性的習(xí)慣后,通常會(huì)出現(xiàn)下列警告。
import { Card } from './Card' // 粉絲請(qǐng)注意,data 中沒(méi)有預(yù)生成的唯一 id const data = [{ text: '關(guān)注' }, { text: '點(diǎn)贊' }, { text: '收藏' }] export default function App() { return ( <div className="container"> {data.map((content, index) => ( <div key={index} className="card"> <Card text={content.text} /> </div> ))} </div> ) }
有時(shí),我們的數(shù)據(jù)不會(huì)附加唯一標(biāo)識(shí)符。一個(gè)簡(jiǎn)單的解決方案就是,使用列表中當(dāng)前元素的索引。雖然但是,使用數(shù)組中元素的索引作為 key
的問(wèn)題在于,它不能代表跨渲染的特定元素。
假設(shè)我們有一個(gè)包含多個(gè)元素的列表,并且用戶(hù)通過(guò)刪除第二個(gè)元素與它們進(jìn)行交互。對(duì)于第一個(gè)元素,其底層 DOM 結(jié)構(gòu)沒(méi)有任何改變;這反映在它的 key
上,它保持不變,第一個(gè)元素的 key
還是索引 0
。
對(duì)于第三個(gè)元素及之后的項(xiàng)目,它們的內(nèi)容沒(méi)有改變,因此它們的底層結(jié)構(gòu)也不應(yīng)該改變。雖然但是,因?yàn)榈诙€(gè)元素被刪除了,所有其他元素的 key
屬性都會(huì)發(fā)生變化,因?yàn)檫@里的 key
是基于數(shù)組索引生成的。React 會(huì)假設(shè)它們已經(jīng)改變并重新計(jì)算它們的結(jié)構(gòu) —— 但這是不必要的。這會(huì)對(duì)性能產(chǎn)生負(fù)面影響,并且還可能導(dǎo)致不一致和不正確的狀態(tài)。
為了搞定這個(gè)問(wèn)題,最重要的是要記住,key
不一定是 id。只要它們是唯一的,并且能代表生成的 DOM 結(jié)構(gòu),我們想使用的任何 key
都問(wèn)題不大。
export default function App() { return ( <div className="container"> {data.map(content => ( <div key={content.text} className="card"> {/* 直接使用內(nèi)容作為 key 也能奏效 */} <Card text={content.text} /> </div> ))} </div> ) }
React Hook useXXX 有條件地調(diào)用。React Hooks 必須在每個(gè)組件渲染中以完全相同的順序調(diào)用
我們可以在開(kāi)發(fā)過(guò)程中以不同的方式優(yōu)化我們的代碼。我們力所能及的一件事就是,確保某些代碼只在需要該代碼的代碼分支中按需執(zhí)行。尤其是在處理時(shí)間或資源密集型的代碼時(shí),這可能會(huì)在性能方面產(chǎn)生巨大的差異。
const Toggle = () => { const [isOpen, setIsOpen] = useState(false) if (isOpen) { return <div>{/* ... */}</div> } const openToggle = useCallback(() => setIsOpen(true), []) return <button onClick={openToggle}>{/* ... */}</button> }
不幸的是,將這種優(yōu)化技術(shù)應(yīng)用于 Hooks,會(huì)向我們提示不要條件調(diào)用 React Hooks 的警告,因?yàn)槲覀儽仨氃诿總€(gè)組件渲染中以相同的順序調(diào)用它們。
這是必要的,因?yàn)樵趦?nèi)部,React 使用 Hook 的調(diào)用順序來(lái)跟蹤其底層狀態(tài),并在渲染之間保留它們。如果我們打亂了這個(gè)順序,React 內(nèi)部將不再知道哪個(gè)狀態(tài)與 Hook 匹配。這會(huì)給 React 帶來(lái)重大問(wèn)題,甚至可能導(dǎo)致錯(cuò)誤。
React Hooks 必須始終在組件的頂層無(wú)條件地調(diào)用。在實(shí)踐中,這通??梢詺w結(jié)為保留組件的第一部分用于 React Hook 初始化。
const Toggle = () => { const [isOpen, setIsOpen] = useState(false) const openToggle = useCallback(() => setIsOpen(true), []) if (isOpen) { return <div>{/* ... */}</div> } return <button onClick={openToggle}>{/* ... */}</button> }
React Hook 缺少依賴(lài):“XXX”。包含它或刪除依賴(lài)數(shù)組
React Hooks 一個(gè)有趣的地方在于依賴(lài)數(shù)組。幾乎每個(gè) React Hook 都接受數(shù)組形式的第二個(gè)參數(shù),我們可以在其中定義 Hook 的依賴(lài)。當(dāng)任何依賴(lài)發(fā)生變化時(shí),React 會(huì)檢測(cè)到它,并重新觸發(fā) Hook。
在官方文檔中,如果變量在 Hook 中使用,React 建議開(kāi)發(fā)者始終將所有變量包含在依賴(lài)數(shù)組中,并在更改時(shí)影響組件的渲染。
為了搞定這個(gè)問(wèn)題,建議在 react-hooks ESLint plugin
中使用 exhaustive-deps rule
。當(dāng)任何 React Hook 未定義所有依賴(lài)時(shí),激活它會(huì)向我們自動(dòng)發(fā)出警告。
const Component = ({ value, onChange }) => { useEffect(() => { if (value) { onChange(value) } }, [value]) // onChange 沒(méi)有作為依賴(lài)包含進(jìn)來(lái) }
我們應(yīng)該深刻認(rèn)識(shí)到,依賴(lài)數(shù)組問(wèn)題的原因與 JS 中閉包和作用域的概念有關(guān)。如果 React Hook 的主回調(diào)使用了其自身作用域之外的變量,那么它只能記住這些變量在執(zhí)行時(shí)的版本。
但是,當(dāng)這些變量發(fā)生更改時(shí),回調(diào)的閉包無(wú)法自動(dòng)獲取這些更改的版本。這可能會(huì)導(dǎo)致使用過(guò)時(shí)的依賴(lài)引用來(lái)執(zhí)行 React Hook 代碼,并導(dǎo)致與預(yù)期不同的行為。
因此,始終建議對(duì)依賴(lài)項(xiàng)數(shù)組進(jìn)行抽絲剝繭。這樣做可以搞定以這種方式調(diào)用 React Hooks 時(shí)可能出現(xiàn)的所有問(wèn)題,因?yàn)樗鼘?React 指向要跟蹤的變量。當(dāng) React 檢測(cè)到任何變量的更改時(shí),它將重新運(yùn)行回調(diào),從而允許它獲取依賴(lài)的更改版本,并按預(yù)期運(yùn)行。
無(wú)法對(duì)已卸載的組件執(zhí)行 React 狀態(tài)更新
在處理組件中的異步數(shù)據(jù)或邏輯流時(shí),我們可能會(huì)在瀏覽器控制臺(tái)中遭遇運(yùn)行時(shí)錯(cuò)誤,告訴我們無(wú)法對(duì)已卸載的組件執(zhí)行狀態(tài)更新。問(wèn)題在于,在組件樹(shù)中的某個(gè)位置,已卸載的組件會(huì)觸發(fā)狀態(tài)更新。
const Component = () => { const [data, setData] = useState(null) useEffect(() => { fetchAsyncData().then(data => setData(data)) }, []) }
這是由依賴(lài)于異步請(qǐng)求的狀態(tài)更新引起的。異步請(qǐng)求在組件生命周期的某個(gè)位置開(kāi)始,比如在 useEffect Hook 內(nèi),但需要一段時(shí)間才能完成。
有多種方案可以搞定此問(wèn)題,所有這些方案都可以歸類(lèi)為兩個(gè)不同的概念。首先,可以跟蹤組件是否已安裝,我們可以據(jù)此執(zhí)行操作。
雖然這能奏效,但不建議這樣做。此方案的問(wèn)題在于,它不必要地保留未安裝組件的引用,這會(huì)導(dǎo)致內(nèi)存泄漏和性能問(wèn)題。
const Component = () => { const [data, setData] = useState(null) const isMounted = useRef(true) useEffect(() => { fetchAsyncData().then(data => { if (isMounted.current) { setData(data) } }) return () => { isMounted.current = false } }, []) }
第二種方案是首選方案,是在組件卸載時(shí)取消異步請(qǐng)求。某些異步請(qǐng)求庫(kù)已經(jīng)有一個(gè)機(jī)制來(lái)取消此類(lèi)請(qǐng)求。如果是這樣,就像在 useEffect
Hook 的清理回調(diào)期間取消請(qǐng)求一樣簡(jiǎn)單。
如果我們沒(méi)有使用這樣的庫(kù),我們可以使用 AbortController
實(shí)現(xiàn)同款需求。這些取消方法的唯一缺陷在于,它們完全依賴(lài)于庫(kù)的實(shí)現(xiàn)或?yàn)g覽器支持。
const Component = () => { const [data, setData] = useState(null) useEffect(() => { const controller = new AbortController() fetch(url, { signal: controller.signal }).then(data => setData(data)) return () => { controller.abort() } }, []) }
重新渲染次數(shù)過(guò)多。React 限制渲染次數(shù),防止無(wú)限循環(huán)
無(wú)限循環(huán)是每個(gè)開(kāi)發(fā)者的大坑,React 開(kāi)發(fā)者也不例外。幸運(yùn)的是,React 可以很好地檢測(cè)它們,并在整個(gè)設(shè)備變得無(wú)響應(yīng)之前向我們發(fā)出警告。
正如警告所示,問(wèn)題在于我們的組件觸發(fā)了太多的重新渲染。當(dāng)組件在很短的時(shí)間內(nèi)排隊(duì)太多狀態(tài)更新時(shí),就會(huì)發(fā)生這種情況。導(dǎo)致無(wú)限循環(huán)的最常見(jiàn)原因在于:
- 直接在渲染中執(zhí)行狀態(tài)更新
- 未向事件處理程序提供正確的回調(diào)
如果我們?cè)庥鐾罹妫?qǐng)務(wù)必檢查組件的這兩個(gè)方面。
const Component = () => { const [count, setCount] = useState(0) setCount(count + 1) // 在渲染中更新?tīng)顟B(tài) return ( <div className="App"> {/* onClick 沒(méi)有接受一個(gè)妥當(dāng)?shù)幕卣{(diào) */} <button onClick={setCount(prevCount => prevCount + 1)}> Increment that counter </button> </div> ) }
對(duì)象作為 React 子元素?zé)o效/函數(shù)作為 React 子元素?zé)o效
在 React 中,我們可以在組件中渲染很多東西到 DOM。選擇幾乎是無(wú)窮無(wú)盡的:所有 HTML 標(biāo)簽、任何 JSX 元素、任意原始 JS 值、先前值的數(shù)組,甚至 JS 表達(dá)式,只要它們被評(píng)估為任何先前值。
盡管如此,不幸的是,React 仍然不接受所有可能作為 React 子元素存在的東西。具體而言,我們無(wú)法將對(duì)象和函數(shù)渲染到 DOM,因?yàn)檫@兩個(gè)數(shù)據(jù)值不會(huì)評(píng)估為 React 可以渲染到 DOM 中的任何有意義的內(nèi)容。因此,任何這樣做的嘗試都會(huì)導(dǎo)致 React 以上述錯(cuò)誤的形式發(fā)出警告。
如果我們?cè)庥鲞@些錯(cuò)誤之一,建議驗(yàn)證我們正在渲染的變量是否是預(yù)期的類(lèi)型。大多數(shù)情況下,這個(gè)問(wèn)題是由于在 JSX 中渲染子元素或變量而引起的,假設(shè)它是一個(gè)原始值,但實(shí)際上,它是一個(gè)對(duì)象或函數(shù)。作為一種預(yù)防方法,擁有適當(dāng)?shù)念?lèi)型系統(tǒng)可以提供很大幫助。
const Component = ({ body }) => ( <div> <h1>{/* */}</h1> {/* 必須確保 body prop 是有效的 React 子元素 */} <div className="body">{body}</div> </div> )
相鄰的 JSX 元素必須包含在封閉標(biāo)簽中
React 最大的優(yōu)勢(shì)之一在于,能夠通過(guò)組合許多較小的組件來(lái)構(gòu)建整個(gè) App。每個(gè)組件都可以以 JSX 的形式定義它應(yīng)該渲染的 UI 部分,這最終有助于 App 的整個(gè) DOM 結(jié)構(gòu)。
const Component = () => ( <div><NiceComponent /></div> <div><GoodComponent /></div> );
由于 React 的復(fù)合性質(zhì),一個(gè)常見(jiàn)的嘗試是在一個(gè)組件的根中返回兩個(gè)僅在另一個(gè)組件中使用的 JSX 元素。雖然但是,這樣做會(huì)令人驚訝地向 React 開(kāi)發(fā)者發(fā)出警告,告訴我們必須將相鄰的 JSX 元素包裝在封閉標(biāo)簽中。
從普通 React 開(kāi)發(fā)者的角度來(lái)看,該組件只會(huì)在另一個(gè)組件內(nèi)部使用。因此,在我們的心智模型中,從組件返回兩個(gè)元素是完全有意義的,因?yàn)闊o(wú)論外部元素是在該組件還是父組件中定義,生成的 DOM 結(jié)構(gòu)都是相同的。
雖然但是,React 無(wú)法做出這個(gè)假設(shè)。該組件有可能在根中使用并破壞 App,因?yàn)樗鼘?dǎo)致無(wú)效的 DOM 結(jié)構(gòu)。
React 開(kāi)發(fā)者應(yīng)該始終將從組件返回的多個(gè) JSX 元素包裝在封閉標(biāo)記中。這可以是一個(gè)元素、一個(gè)組件或 React 的 Fragment
,如果你確定該組件不需要外部元素。
const Component = () => ( <React.Fragment> <div> <NiceComponent /> </div> <div> <GoodComponent /> </div> </React.Fragment> )
總結(jié)
在開(kāi)發(fā)過(guò)程中邂逅 bug 是不可避免的一部分。雖然但是,我們處理這些錯(cuò)誤消息的方案也表明了作為 React 開(kāi)發(fā)者的技術(shù)修養(yǎng)。為了正確地做到這一點(diǎn),有必要了解這些錯(cuò)誤并知道它們發(fā)生的原因。
本文科普了我們?cè)?React 開(kāi)發(fā)過(guò)程中會(huì)遭遇的八大最常見(jiàn)的 React 錯(cuò)誤消息。我們介紹了錯(cuò)誤消息背后的含義、潛在錯(cuò)誤、如何解決錯(cuò)誤,以及如果不修復(fù)錯(cuò)誤會(huì)發(fā)生什么。
有了這些知識(shí)儲(chǔ)備,我們現(xiàn)在可以更徹底地理解這些錯(cuò)誤,并感到有能力編寫(xiě)更少的包含這些錯(cuò)誤的代碼,從而產(chǎn)生更高質(zhì)量的代碼。
以上就是React八大常見(jiàn)錯(cuò)誤及其解決方案的詳細(xì)內(nèi)容,更多關(guān)于React常見(jiàn)錯(cuò)誤的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
在react-router4中進(jìn)行代碼拆分的方法(基于webpack)
這篇文章主要介紹了在react-router4中進(jìn)行代碼拆分的方法(基于webpack),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03在React項(xiàng)目中使用TypeScript詳情
這篇文章主要介紹了在React項(xiàng)目中使用TypeScript詳情,文章通過(guò)圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09React useMemo與useCallabck有什么區(qū)別
useCallback和useMemo是一樣的東西,只是入?yún)⒂兴煌瑄seCallback緩存的是回調(diào)函數(shù),如果依賴(lài)項(xiàng)沒(méi)有更新,就會(huì)使用緩存的回調(diào)函數(shù);useMemo緩存的是回調(diào)函數(shù)的return,如果依賴(lài)項(xiàng)沒(méi)有更新,就會(huì)使用緩存的return2022-12-12React-Native之TextInput組件的設(shè)置以及如何獲取輸入框的內(nèi)容
這篇文章主要介紹了React-Native之TextInput組件的設(shè)置以及如何獲取輸入框的內(nèi)容問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05React?Router?v6路由懶加載的2種方式小結(jié)
React?Router?v6?的路由懶加載有2種實(shí)現(xiàn)方式,1是使用react-router自帶的?route.lazy,2是使用React自帶的?React.lazy,下面我們就來(lái)看看它們的具體實(shí)現(xiàn)方法吧2024-04-04