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

