React最佳實踐
本文來分享 React 中的 16 種常見反模式和最佳實踐。
1、在組件外部聲明CSS
如果使用 CSS in JS 的解決方案,盡量避免在組件內(nèi)聲明 CSS。
import makeCss from 'some/css/in/js/library'
const Component = () => {
// 不要這樣寫
return <div className={makeCss({ background: red, width: 100% })} />
}
因為在每次渲染時都會重新創(chuàng)建對象,可以將其從組件中提出來:
import cssLibrary from 'some/css/in/js/library'
const someCssClass = makeCss({
background: red,
width: 100%
})
const Component = () => {
return <div className={someCssClass} />
}
2、使用 useCallback 防止函數(shù)重新創(chuàng)建
每當重新渲染 React 組件時,都會重新創(chuàng)建組件中的所有函數(shù)。React 提供了一個 useCallback Hook,可以用來避免這種情況。只要其依賴項不改變,useCallback 就會在渲染之間保留函數(shù)的舊實例。
import { useCallback } from 'react'
const Component = () => {
const [value, setValue] = useState(false)
// 該函數(shù)將在每次渲染時重新創(chuàng)建
const handleClick = () => {
setValue(true)
}
return <button onClick={handleClick}>Click</button>
}
import { useCallback } from 'react'
const Component = () => {
const [value, setValue] = useState(false)
// 僅當變量值更新時才會重新創(chuàng)建此函數(shù)
const handleClick = useCallback(() => {
setValue(true)
}, [value])
return <button onClick={handleClick}>Click</button>
}
對于示例中的小函數(shù),不能保證將函數(shù)包裝在useCallback中確實更好。所以還需要根據(jù)實際情況來判斷是否需要使用 useCallback 包裝。
在底層,React將在每次渲染時檢查依賴關(guān)系,以確定是否需要創(chuàng)建新函數(shù),而且有時依賴關(guān)系經(jīng)常發(fā)生變化。因此,useCallback提供的優(yōu)化并不總是必需的。然而,如果函數(shù)的依賴項不經(jīng)常更新,那么使用useCallback是一種很好的優(yōu)化方法,以避免在每次渲染時重新創(chuàng)建函數(shù)。
3、使用 useCallback 防止依賴項更改
useCallback 不僅可以用于避免函數(shù)實例化,但它也可用于更重要的事情。由于 useCallback 在渲染之間為包裝函數(shù)保留相同的內(nèi)存引用,因此它可用于優(yōu)化其他 useCallback 和記憶的使用。
import { memo, useCallback, useMemo } from 'react'
const MemoizedChildComponent = memo({ onTriggerFn }) => {
// ...
})
const Component = ({ someProp }) => {
// 僅當 someProp 發(fā)生變化時,對 onTrigger 函數(shù)的引用才會發(fā)生變化
const onTrigger = useCallback(() => {
// ...
}, [someProp])
// 這個記憶值只會在 onTrigger 函數(shù)更新時更新
// 如果 onTrigger 不是 useCallback 中的包裝器,則將在每次渲染時重新計算該值
const memoizedValue = useMemo(() => {
// ...
}, [onTrigger])
// Memoize子組件只會在onTrigger函數(shù)更新時重新渲染
// 如果 onTrigger 未包裝在 useCallback 中,MemoizedChildComponent 將在每次渲染此組件時重新渲染
return (<>
<MemoizedChildComponent onTriggerFn={onTrigger} />
<button onClick={onTrigger}>Click me</button>
</>)
}
4、使用 useCallback 防止 useEffect 觸發(fā)
前面的示例展示了如何借助 useCallback 來優(yōu)化渲染,同樣,也可以避免不必要的 useEffect 觸發(fā)。
import { useCallback, useEffect } from 'react'
const Component = ({ someProp }) => {
// 僅當 someProp 發(fā)生變化時,對 onTrigger 函數(shù)的引用才會發(fā)生變化
const onTrigger = useCallback(() => {
// ...
}, [someProp])
// useEffect 僅在 onTrigger 函數(shù)更新時運行
// 如果 onTrigger 未包裝在 useCallback 中,則 useEffect 將在每次此函數(shù)渲染時運行
useEffect(() => {
// ...
}, [onTrigger])
return <button onClick={onTrigger}>Click me</button>
}
5、當不需要依賴項時,向 useEffect 添加空依賴項
如果 effect 不依賴于任何變量,可以將空依賴項數(shù)組作為 useEffect 的第二個參數(shù)。否則,effect 將在每次渲染時運行。
import { useEffect } from 'react'
const Component = () => {
useEffect(() => {
// ...
}, [])
return <div>Example</div>
}
這個邏輯也適用于其他 React hook,例如 useCallback 和 useMemo。不過,如果沒有任何依賴項,可能根本不需要使用這些 Hooks。
6、始終將所有依賴項添加到 useEffect 和其他 React Hooks
在處理內(nèi)置 React Hooks 的依賴項項(例如 useEffects 和 useCallback)時,請將所有依賴項添加到依賴項列表(Hooks 的第二個參數(shù))。當省略依賴項時,effect 或回調(diào)可能會使用它的舊值,這通常會導致難以預測的錯誤。
import { useEffect } from 'react'
const Component = () => {
const [value, setValue] = useState()
useEffect(() => {
// 使用 value 變量的代碼
// 將變量添加到依賴項數(shù)組中,應在此處添加 value 變量
}, [])
return <div>{value}</div>
}
那當 useEffect 被觸發(fā)的次數(shù)比希望的次數(shù)多時,如何避免副作用?不幸的是,沒有完美的解決方案。不同的場景需要不同的解決方案。可以嘗試使用 hook 僅運行一次代碼,這有時很有用,但實際上并不是一個值得推薦的解決方案。
大多數(shù)情況下,可以使用 if-case 來解決問題??梢圆榭串斍盃顟B(tài)并從邏輯上決定是否確實需要運行代碼。例如,如果不將 value 值添加為上述 effect 的依賴項的原因是僅在值未定義時運行代碼,則只需在 effect 內(nèi)添加 if 語句即可。
import { useEffect } from 'react'
const Component = () => {
const [value, setValue] = useState()
useEffect(() => {
if (!value) {
// ...
}
}, [value])
return <div>{value}</div>
其他場景可能更復雜,也許使用 if 語句來防止 effect 多次發(fā)生不太可行。在這種情況下,首先應該確定,真的需要 effect 嗎?在很多情況下,開發(fā)人員在實際上不應該這樣做時卻使用了 effect。
7、不要將外部函數(shù)包裝在 useCallback 中
不需要 useCallback 來調(diào)用外部函數(shù)。只需按原樣調(diào)用外部函數(shù)即可。這使得 React 不必檢查 useCallback 是否需要重新創(chuàng)建,并且使代碼更簡潔。
import { useCallback } from 'react'
import externalFunction from '/services/externalFunction'
const Component = () => {
// ?
const handleClick = useCallback(() => {
externalFunction()
}, [])
return <button onClick={handleClick}>Click me</button>
}
import externalFunction from '/services/externalFunction'
const Component = () => {
// ?
return <button onClick={externalFunction}>Click me</button>
}
使用 useCallback 的一個用例是回調(diào)調(diào)用多個函數(shù)或讀取或更新內(nèi)部狀態(tài)(例如 useState hook 中的值或組件傳入的 props 之一)時。
import { useCallback } from 'react'
import { externalFunction, anotherExternalFunction } from '/services'
const Component = ({ passedInProp }) => {
const [value, setValue] = useState()
const handleClick = useCallback(() => {
// 調(diào)用了多個函數(shù)
externalFunction()
anotherExternalFunction()
// 讀取和或設置內(nèi)部值或?qū)傩浴? setValue(passedInProp)
}, [passedInProp, value])
return <button onClick={handleClick}>Click me</button>
}
8、不要將 useMemo 與空依賴數(shù)組一起使用
如果添加了帶有空依賴項數(shù)組的 useMemo,問問自己為什么要這樣做。因為它依賴于組件的狀態(tài)變量而不想添加它?在這種情況下,應該列出所有依賴變量!因為 useMemo 沒有任何依賴項?那就不需要使用 useMemo 了。
import { useMemo } from 'react'
const Component = () => {
// ?
const memoizedValue = useMemo(() => {
return 3 + 5
}, [])
return <div>{memoizedValue}</div>
}
// ?
const memoizedValue = 3 + 5
const Component = () => {
return <div>{memoizedValue}</div>
}
9、不要在其他組件中聲明組件
const Component = () => {
// ?
const ChildComponent = () => {
return <div>child</div>
}
return <div><ChildComponent /></div>
}
這樣寫的話,組件內(nèi)聲明的變量將在每次組件呈現(xiàn)時重新聲明。在這種情況下,這意味著每次父級重新渲染時都必須重新創(chuàng)建功能子組件。就必須在每次渲染時實例化一個函數(shù)。React 將無法決定何時進行任何類型的組件優(yōu)化。如果在 ChildComponent 中使用 hooks,它們將在每次渲染時重新啟動。
那該怎么辦呢?只需在父組件之外聲明子組件即可。
const ChildComponent = () => {
return <div>child</div>
}
const Component = () => {
return <div><ChildComponent /></div>
}
或者,更好的方式是:
import ChildComponent from 'components/ChildComponent'
const Component = () => {
return <div><ChildComponent /></div>
}
10、不要在 If 語句中使用 Hook
在React內(nèi)部,Hook的調(diào)用順序是必須固定的,以確保正確地管理組件狀態(tài)和生命周期。如果在if語句內(nèi)部使用Hook,會導致兩個問題:
違反Hook的調(diào)用規(guī)則:根據(jù)React的規(guī)定,Hook應該在每次渲染中按照相同的順序被調(diào)用。當條件發(fā)生變化時,Hook調(diào)用的順序可能會發(fā)生變化,這會破壞React對Hook調(diào)用順序的依賴,導致無法預料的行為和錯誤。
Hook的依賴關(guān)系無效:Hook的工作原理是基于依賴項列表,它可以檢測依賴項的變化,并在需要時重新運行。如果將Hook放在if語句中,它的依賴關(guān)系可能無法正確地捕捉到變化,從而導致狀態(tài)更新或副作用的錯誤。
import { useState } from 'react'
const Component = ({ propValue }) => {
if (!propValue) {
const [value, setValue] = useState(propValue)
}
return <div>{value}</div>
}
11、使用 useState 而不是變量
在React中,存儲狀態(tài)應該始終使用 React hooks(如useState或useReducer),不要直接將狀態(tài)聲明為組件中的變量。這樣做會導致在每次渲染時重新聲明變量,這意味著React無法像通常一樣對其進行記憶化處理。
import AnotherComponent from 'components/AnotherComponent'
const Component = () => {
const value = { someKey: 'someValue' }
return <AnotherComponent value={value} />
}
在上述情況下,依賴于value的AnotherComponent及其相關(guān)內(nèi)容將在每次渲染時重新渲染,即使它們使用memo、useMemo或useCallback進行了記憶化處理。
如果將一個帶有value作為依賴的useEffect添加到組件中,它將在每次渲染時觸發(fā)。因為每次渲染時 value 的JavaScript引用都會不同。
通過使用 React 的useState,React會保留value的相同引用,直到使用setValue進行更新。然后,React 將能夠檢測何時觸發(fā)和何時不觸發(fā) effect ,并重新計算記憶化處理。
import { useState } from 'react'
import AnotherComponent from 'components/AnotherComponent'
const Component = () => {
const [value, setValue] = useState({ someKey: 'someValue' })
return <AnotherComponent value={value} />
}
如果只需要一個狀態(tài),在初始化后就不再更新,那么可以將變量聲明在組件外部。這樣 JavaScript 引用將不會改變。
// 如果不需要更新該值,就可以這樣聲明變量
const value = { someKey: 'someValue' }
const Component = () => {
return <AnotherComponent value={value} />
}
12、return 后不使用 Hook
根據(jù)定義,if語句是有條件執(zhí)行的,“return”關(guān)鍵字也會導致條件 Hook 渲染。
import { useState } from 'react'
const Component = ({ propValue }) => {
if (!propValue) {
return null
}
// 這個 hook 是有條件的,因為只有當 propValue 存在時才會調(diào)用它
const [value, setValue] = useState(propValue)
return <div>{value}</div>
}
條件語句中的 return 語句會使后續(xù)的 Hook 成為有條件的。為了避免這種情況,將所有的 Hook 放在組件的第一個條件渲染之前。也就是說,始終將 Hook 放在組件的頂部。
import { useState } from 'react'
const Component = ({ propValue }) => {
// 在條件渲染之前放 hooks
const [value, setValue] = useState(propValue)
if (!propValue) {
return null
}
return <div>{value}</div>
}
13、讓子組件決定是否應該渲染
在許多情況下應該讓子組件決定是否應該渲染:
import { useState } from 'react'
const ChildComponent = ({ shouldRender }) => {
return <div>Rendered: {shouldRender}</div>
}
const Component = () => {
const [shouldRender, setShouldRender] = useState(false)
return <>
{ !!shouldRender && <ChildComponent shouldRender={shouldRender} /> }
</>
}
以上是有條件地渲染子組件的常見方法。代碼很好,除了在有很多子組件時有點冗長之外。但根據(jù) ChildComponent 的作用,可能存在更好的解決方案。下面來稍微重寫一下代碼。
import { useState } from 'react'
const ChildComponent = ({ shouldRender }) => {
if (!shouldRender) {
return null
}
return <div>Rendered: {shouldRender}</div>
}
const Component = () => {
const [shouldRender, setShouldRender] = useState(false)
return <ChildComponent shouldRender={shouldRender} />
}
這里重寫了兩個組件,將條件渲染移至子組件中。那條件渲染移至子組件有什么好處?
最大的好處是 React 可以繼續(xù)渲染 ChildComponent,即使它不可見。這意味著,ChildComponent 可以在隱藏時保持其狀態(tài),然后第二次渲染而不會丟失其狀態(tài)。它一直都在那里,只是不可見。
如果組件像第一個代碼那樣停止渲染,則 useState 中保存的狀態(tài)將被重置,并且一旦組件再次渲染,useEffect、useCallback 和 useMemo 都需要重新運行并重新計算新值。
如果代碼會觸發(fā)一些網(wǎng)絡請求或進行一些復雜的計算,那么當組件再次渲染時,這些請求也會運行。同樣,如果將一些表單數(shù)據(jù)存儲在組件的內(nèi)部狀態(tài)中,則每次組件隱藏時都會重置。
14、使用 useReducer 而不是多個 useState
可以使用一個 useReducer 來代替使用多個 useState,這樣寫起來可能比較麻煩,但是這樣既可以避免不必要的渲染,又可以讓邏輯更容易理解。一旦有了 useReducer,向組件添加新邏輯和狀態(tài)就會容易得多。
import { useState } from 'react'
const Component = () => {
// ?
const [text, setText] = useState(false)
const [error, setError] = useState('')
const [touched, setTouched] = useState(false)
const handleChange = (event) => {
const value = event.target.value
setText(value)
if (value.length < 6) {
setError('Too short')
} else {
setError('')
}
}
return <>
{!touched && <div>Write something...</div> }
<input type="text" value={text} onChange={handleChange} />
<div>Error: {error}</div>
</>
}
import { useReducers } from 'react'
const UPDATE_TEXT_ACTION = 'UPDATE_TEXT_ACTION'
const RESET_FORM = 'RESET_FORM'
const getInitialFormState = () => ({
text: '',
error: '',
touched: false
})
const formReducer = (state, action) => {
const { data, type } = action || {}
switch (type) {
case UPDATE_TEXT_ACTION:
const text = data?.text ?? ''
return {
...state,
text: text,
error: text.length < 6,
touched: true
}
case RESET_FORM:
return getInitialFormState()
default:
return state
}
}
const Component = () => {
// ?
const [state, dispatch] = useReducer(formReducer, getInitialFormState());
const { text, error, touched } = state
const handleChange = (event) => {
const value = event.target.value
dispatch({ type: UPDATE_TEXT_ACTION, text: value})
}
return <>
{!touched && <div>Write something...</div> }
<input type="text" value={text} onChange={handleChange} />
<div>Error: {error}</div>
</>
}
15、將初始狀態(tài)寫為函數(shù)而不是對象
來看看下面的 getInitialFormState 函數(shù):
// 初始狀態(tài)是一個函數(shù)
const getInitialFormState = () => ({
text: '',
error: '',
touched: false
})
const formReducer = (state, action) => {
// ...
}
const Component = () => {
const [state, dispatch] = useReducer(formReducer, getInitialFormState());
// ...
}
這里將將初始狀態(tài)寫成了一個函數(shù),但其實直接使用一個對象也是可以的。
// 初始狀態(tài)是一個對象
const initialFormState = {
text: '',
error: '',
touched: false
}
const formReducer = (state, action) => {
// ...
}
const Component = () => {
const [state, dispatch] = useReducer(formReducer, getInitialFormState());
// ...
}
那為什么不直接寫成對象呢?答案很簡單,避免可變性。在上面的例子中,當initialFormState是一個對象時,我們可能會一不小心就在代碼中的某個地方改變了該對象。如果這樣,當再次使用該變量,例如在重置表單時,將無法恢復初始狀態(tài)。相反,會得到變異的對象。
因此,將初始狀態(tài)轉(zhuǎn)換為返回初始狀態(tài)對象的 getter 函數(shù)是一個很好的做法?;蛘吒玫氖牵褂孟?nbsp;Immer 這樣的庫,它用于避免編寫可變代碼。
16、當組件不應重新渲染時,使用 useRef 而不是 useState
可以通過用 useRef 替換 useState 來優(yōu)化組件渲染。來看下面的例子:
import { useEffect } from 'react'
const Component = () => {
const [triggered, setTriggered] = useState(false)
useEffect(() => {
if (!triggered) {
setTriggered(true)
// ...
}
}, [triggered])
}
當運行上面的代碼時,組件將在調(diào)用 setTriggered 時重新渲染。在這種情況下,觸發(fā)狀態(tài)變量可能是確保 effect 僅運行一次的一種方法。
由于在這種情況下觸發(fā)變量的唯一用途是跟蹤函數(shù)是否已被觸發(fā),因此不需要組件渲染任何新狀態(tài)。因此,可以將 useState 替換為 useRef,這樣更新時就不會觸發(fā)組件重新渲染。
import { useRef } from 'react'
const Component = () => {
const triggeredRef = useRef(false)
useEffect(() => {
if (!triggeredRef.current) {
triggeredRef.current = true
// ...
}
}, [])
}
那為什么需要使用 useRef,而不簡單地使用組件外部的變量呢?
const triggered = false
const Component = () => {
useEffect(() => {
if (!triggered) {
triggered = true
// ...
}
}, [])
}
這里需要 useRef 的原因是因為上面的代碼不能以同樣的方式工作!上面的變量只會為 false 一次。如果組件卸載了,當組件再次掛載時,triggered變量仍然會被設置為true,因為triggered變量并沒有綁定到React的生命周期中。當使用 useRef 時,React 將在組件卸載并再次安裝時重置其值。在這種情況下,就可以要使用 useRef。