React 使用中值得優化的 7 個點
自從使用React
後,見過越來越多可值得優化的點,比如:
-
大量的
props
-
props
的不相容性 -
props
複製為state
-
返回
JSX
的函式 -
state
的多個狀態 -
useState
過多 -
複雜的
useEffect
大量的 props
如果需要把大量的props
傳遞到一個元件中,那麼很有可能 該元件可再進一步拆分。
問題來了,“大量” 具體是多少呢?答案是 看情況。
假設你正在開發 一個包含 20 個或更多props
的元件時,你想再新增一些props
完善其他功能,這時有兩點可以參考 是否應拆分元件:
該元件是否做了多件事?
像函式一樣,一個元件應該只做好一件事,所以考慮下 將元件拆分成多個小元件是否會更好。
例如,該元件存在props
的不相容性或返回JSX
的函式。
該元件是否可被合成?
開發中,組合是一種很好的模式但經常被忽視。
如果你的元件中存在將不相干邏輯塞到一起的情況,是時候考慮使用組合了。
假設我們有一個表單元件來處理某組織的使用者資訊:
<ApplicationForm user={userData} organization={organizationData} categories={categoriesData} locations={locationsData} onSubmit={handleSubmit} onCancel={handleCancel} ... />
通過該元件的props
,我們可看到它們都與元件提供的功能密切相關。
該元件看起來並無大礙,但如果將其中的一些props
分擔到子元件,那麼資料流就會更清晰。
<ApplicationForm onSubmit={handleSubmit} onCancel={handleCancel}> <ApplicationUserForm user={userData} /> <ApplicationOrganizationForm organization={organizationData} /> <ApplicationCategoryForm categories={categoriesData} /> <ApplicationLocationsForm locations={locationsData} /> </ApplicationForm>
現在,我們已經看到該表單元件只處理提交和取消動作,其他範圍內的事情,都交給了對應的子元件。
是否傳遞了很多有關配置的props
在某些情況下,將多個有關配置的props
組合成一個options
是個不錯的實踐。
假設我們有一個可顯示某種表格的元件:
<Grid data={gridData} pagination={false} autoSize={true} enableSort={true} sortOrder="desc" disableSelection={true} infiniteScroll={true} ... />
我們可以很清楚地看出,該元件除了data
外其餘的props
都是與配置有關的。
如果將多個配置props
合成為一個options
,就可更好地控制組件的選項,規範性也得到提升。
const options = { pagination: false, autoSize: true, enableSort: true, sortOrder: 'desc', disableSelection: true, infiniteScroll: true, ... } <Grid data={gridData} options={options} />
props 的不相容性
避免元件之間傳遞不相容的props
。
假設你的元件庫中有一個<Input />
元件,而該元件開始時僅用於處理文字,但過了一段時間後,你將它用於電話號碼處理。
你的實現可能如下:
function Input({ value, isPhoneNumberInput, autoCapitalize }) { if (autoCapitalize) capitalize(value) return <input value={value} type={isPhoneNumberInput ? 'tel' : 'text'} /> }
問題在於,isPhoneNumberInput
與autoCapitalize
之間並不存在關聯,將一個手機號首字母大寫是沒有任何意義的。
在這種情況下,我們可以將其分割成多個小元件,來明確具體的職責,如果有共享邏輯,可以將其放到hooks
中。
function TextInput({ value, autoCapitalize }) { if (autoCapitalize) capitalize(value) useSharedInputLogic() return <input value={value} type="text" /> } function PhoneNumberInput({ value }) { useSharedInputLogic() return <input value={value} type="tel" /> }
雖然上面例子有點勉強,可當發現元件的props
存在不相容性時,是時候考慮拆分元件了。
props 複製為 state
如何更好地將props
作為state
的初始值。
有如下元件:
function Button({ text }) { const [buttonText] = useState(text) return <button>{buttonText}</button> }
該元件將text
作為useState
的初始值,可能會導致意想不到的行為。
實際上該元件已經關掉了props
的更新通知,如果text
在上層被更新,它將仍呈現 接受到text
的第一次值,這更容易使元件出錯。
一個更實際場景是,我們想基於props
通過大量計算來得到新的state
。
在下面的例子中,slowlyFormatText
函式用於格式化text
,注意 需要很長時間才能完成。
function Button({ text }) { const [formattedText] = useState(() => slowlyFormatText(text)) return <button>{formattedText}</button> }
解決此問題 最好的方案是 使用useMemo
代替useState
。
function Button({ text }) { const formattedText = useMemo(() => slowlyFormatText(text), [text]) return <button>{formattedText}</button> }
現在slowFormatFormat
僅在text
更改時執行,並且沒有阻斷 上層元件更新。
進一步閱讀:Writing resilient components by Dan Abramov。
返回 JSX 的函式
不要從元件內部的函式中返回JSX
。
這種模式雖然很少出現,但我還是時不時碰到。
僅舉一個例子來說明:
function Component() { const topSection = () => { return ( <header> <h1>Component header</h1> </header> ) } const middleSection = () => { return ( <main> <p>Some text</p> </main> ) } const bottomSection = () => { return ( <footer> <p>Some footer text</p> </footer> ) } return ( <div> {topSection()} {middleSection()} {bottomSection()} </div> ) }
該例子雖然看起來沒什麼問題,但其實這會破壞程式碼的整體性,使維護變得困難。
要麼把函式返回的JSX
直接內聯到元件內,要麼將其拆分成一個元件。
有一點需要注意,如果你建立了一個新元件,不必將其移動到新檔案中的。
如果多個元件緊密耦合,將它們儲存在同一個檔案中是有意義的。
state 的多個狀態
避免使用多個布林值來表示元件狀態。
當編寫一個元件並多次迭代後,很容易出現這樣一種情況,即內部有多個布林值來表示 該元件處於哪種狀態。
比如下面的例子:
function Component() { const [isLoading, setIsLoading] = useState(false) const [isFinished, setIsFinished] = useState(false) const [hasError, setHasError] = useState(false) const fetchSomething = () => { setIsLoading(true) fetch(url) .then(() => { setIsLoading(false) setIsFinished(true) }) .catch(() => { setHasError(true) }) } if (isLoading) return <Loader /> if (hasError) return <Error /> if (isFinished) return <Success /> return <button onClick={fetchSomething} /> }
當按鈕被點選時,我們將isLoading
設定為true
,並通過fetch
執行網路請求。
如果請求成功,我們將isLoading
設定為false
,isFinished
設定為true
,如果有錯誤,將hasError
設定為true
。
雖然這在技術上是可行的,但很難推斷出元件處於什麼狀態,而且不容易維護。
並且有可能最終處於“不可能的狀態”,比如我們不小心同時將isLoading
和isFinished
設定為true
。
解決此問題一勞永逸的方案是 使用列舉來管理狀態。
在其他語言中,列舉是一種定義變數的方式,該變數只允許設定為預定義的常量值集合,雖然在JavaScript
中不存在列舉,但我們可以使用字串作為列舉:
function Component() { const [state, setState] = useState('idle') const fetchSomething = () => { setState('loading') fetch(url) .then(() => { setState('finished') }) .catch(() => { setState('error') }) } if (state === 'loading') return <Loader /> if (state === 'error') return <Error /> if (state === 'finished') return <Success /> return <button onClick={fetchSomething} /> }
通過這種方式,完全杜絕了出現 不可能狀態的情況,並更利用擴充套件。
如果你使用TypeScript
開發的話,則可以從定義時就實現列舉:
const [state, setState] = useState<'idle' | 'loading' | 'error' | 'finished'>('idle')
useState 過多
避免在同一個元件中使用太多的useState
。
一個包含許多useState
的元件可能會做多件事情,可以考慮是否要拆分它。
當然也存在一些複雜的場景,我們需要在元件中管理一些複雜的狀態。
下面是自動輸入元件的例子:
function AutocompleteInput() { const [isOpen, setIsOpen] = useState(false) const [inputValue, setInputValue] = useState('') const [items, setItems] = useState([]) const [selectedItem, setSelectedItem] = useState(null) const [activeIndex, setActiveIndex] = useState(-1) const reset = () => { setIsOpen(false) setInputValue('') setItems([]) setSelectedItem(null) setActiveIndex(-1) } const selectItem = (item) => { setIsOpen(false) setInputValue(item.name) setSelectedItem(item) } ... }
我們有一個reset
函式,可以重置所有狀態,還有一個selectItem
函式,可更新一些狀態。
這些函式都離不開useState
定義的狀態。如果功能繼續迭代,那麼函式就會越來越多,狀態也會隨之增加,資料流就會變得模糊不清。
在這種情況下,使用useReducer
來代替 過多的useState
是一個不錯的選擇。
const initialState = { isOpen: false, inputValue: "", items: [], selectedItem: null, activeIndex: -1 } function reducer(state, action) { switch (action.type) { case "reset": return { ...initialState } case "selectItem": return { ...state, isOpen: false, inputValue: action.payload.name, selectedItem: action.payload } default: throw Error() } } function AutocompleteInput() { const [state, dispatch] = useReducer(reducer, initialState) const reset = () => { dispatch({ type: 'reset' }) } const selectItem = (item) => { dispatch({ type: 'selectItem', payload: item }) } ... }
通過使用reducer
,我們封裝了管理狀態的邏輯,並將複雜的邏輯移出了元件,這使得元件更容易維護。
進一步閱讀:state reducer pattern by Kent C. Dodds。
複雜的 useEffect
避免在useEffect
中做太多事情,它們使程式碼易於出錯,並且難以推理。
下面的例子中 犯了一個很大的錯誤:
function Post({ id, unlisted }) { ... useEffect(() => { fetch(`/posts/${id}`).then(/* do something */) setVisibility(unlisted) }, [id, unlisted]) ... }
當unlisted
改變時,即使id
沒有變,也會呼叫fetch
。
正確的寫法應該是 將多個依賴分離:
function Post({ id, unlisted }) { ... useEffect(() => { // when id changes fetch the post fetch(`/posts/${id}`).then(/* ... */) }, [id]) useEffect(() => { // when unlisted changes update visibility setVisibility(unlisted) }, [unlisted]) ... }