你必須避免的 10 個 React 錯誤用法
本文是作者在實際工作經(jīng)驗中總結(jié)提煉出的錯誤使用 React 的一些方式,希望能夠幫助你擺脫這些相同的錯誤。
1. Props 透傳
props 透傳是將單個 props 從父組件向下多層傳遞的做法。理想狀態(tài)下,props 不應(yīng)該超過兩層。當(dāng)我們選擇多層傳遞時,會導(dǎo)致一些性能問題,這也讓 React 官方比較頭疼。props 透傳會導(dǎo)致不必要的重新渲染。因為 React 組件總會在 props 發(fā)生變化時重新渲染,而那些不需要 props,只是提供傳遞作用的中間層組件都會被渲染。除了性能問題外,props 透傳會導(dǎo)致數(shù)據(jù)難以跟蹤,對很多試圖看懂代碼的人來說也是一種很大的挑戰(zhàn)。
const A = () => {
const [title, setTitle] = useState('')
return <B title={title} />
}
const B = ({ title }) => {
return <C title={title} />
}
const C = ({ title }) => {
return <D title={title} />
}
const D = ({ title }) => {
return <div>{title}</div>
}
解決這個問題的方法有很多,比如 React Context Hook,或者類似 Redux 的庫。但是使用 Redux 需要額外編寫一些代碼,它更適合單個狀態(tài)改變很多東西的復(fù)雜場景。簡單的項目選擇使用 Context Hook 是更好的選擇。
2. 導(dǎo)入代碼超出實際所用的代碼
React 是一個前端框架,它有著不小的代碼體積。我們在編寫 React 程序時,應(yīng)該避免導(dǎo)入很多用不到的模塊。因為它們也會被打包到運(yùn)行時代碼發(fā)送到用戶的客戶端/瀏覽器/移動設(shè)備上。額外的依賴會導(dǎo)致應(yīng)用的體積膨脹,增加用戶的加載時間,讓網(wǎng)頁變慢,降低用戶體驗度。
import _ from 'lodash' // 整個包導(dǎo)入
import _map from 'lodash/map' // 只導(dǎo)入需要的包
為了保證良好的用戶體驗度,我們應(yīng)該讓 FCP 保持在 1.8 秒以內(nèi),所以我們需要簡化代碼體積?,F(xiàn)代的打包工具都有搖樹功能,使用各種方式來縮小和壓縮我們用于生產(chǎn)的代碼,比如 webpack。但是在有些情況下它不能很好的去處無用的代碼,我們最好知道那些代碼應(yīng)該被打包,而不是僅僅依靠打包工具來嘗試修復(fù)我們的代碼問題?,F(xiàn)在的 JavaScript 已經(jīng)經(jīng)歷了多次重大更新,擁有了非常多的新功能。在過去我們需要使用 lodash 這類庫來實現(xiàn)這些功能,但是現(xiàn)在 lodash 的優(yōu)勢在慢慢減少。在 youmightnotneed.com/lodash/ 上面可以查看如何使用現(xiàn)代 JavaScript 取代 lodash。當(dāng)然這取決于你的用戶是使用什么版本的瀏覽器和 JavaScript。但是我們大都會用 babel 或者類似的轉(zhuǎn)譯器來處理這個問題。而且現(xiàn)在幾乎每個人都在用 Chrome 了,對吧?其他庫也是同樣的道理。
3. 不要將業(yè)務(wù)邏輯和組件邏輯分離
在過去,很多人認(rèn)為 React 組件應(yīng)該包含邏輯,邏輯是組件的一部分。但是拿到今天來看,這個觀點(diǎn)是有問題的。
const Example = () => {
const [data, setData] = useState([])
useEffect(() => {
fetch('...')
.then(res => res.json())
.then(data => {
const filteredData = data.filter(item => item.status === ture)
setData(filteredData)
})
}, [])
return <div>...</div>
}
將組件和邏輯放到一起會讓組件變得復(fù)雜,當(dāng)修改或者增加業(yè)務(wù)邏輯時,對開發(fā)者來說更加復(fù)雜,而且想了解整個流程也更加具有挑戰(zhàn)性。
const Example = () => {
const { data, error } = useData()
return <div>...</div>
}
將組件和邏輯分離,有兩個好處:
關(guān)注分離點(diǎn)。
重用業(yè)務(wù)邏輯。
4. 每次渲染的重復(fù)工作
即使你是經(jīng)驗豐富的 React 老手,可能仍然做不到對渲染這件事完全了解。渲染是經(jīng)常發(fā)生并且很多時候是出乎意料的。這是使用 React 編寫組件的核心原則之一,在編寫 React 組件時應(yīng)該牢記在心。同時意味著,在渲染組件的時候會重新執(zhí)行某些邏輯。React 提供了 useMemo 和 useCallback 兩個 Hook,如果使用得當(dāng),這些 Hook 可以緩存計算結(jié)果或者函數(shù),來減少不必要的重復(fù)渲染,最終提高性能。
import React, { useMemo } from 'react'
const MemoExample = ({ items, filter }) => {
const filteredItems = useMemo(() => {
return items.filter(filter )
}, [filter, items])
return filteredItems.map(item => <p>{item}</p>)
}
上面的例子是一個項目列表的展示,其中需要通過某些條件來過濾列表,最終展示給用戶。這種數(shù)據(jù)過濾在前端中是不可避免的,所以我們可以使用 useMemo 來緩存過濾數(shù)據(jù)的過程,這樣只有當(dāng) items 和 filter 發(fā)生變化時它才會重新渲染。
5. useEffect 使用不當(dāng)
useEffect 是 React 中使用率最高的 Hooks 之一。在 class 組件的時代,componentDidMount 是一個通用的生命周期函數(shù),用來做一些數(shù)據(jù)請求,事件綁定等。在 Hooks 時代,useEffect 已經(jīng)取代了它。但是不正確的使用 useEffect 可能會導(dǎo)致最終創(chuàng)建多個事件綁定。下面就是一個錯誤的用法。
import React, { useMemo } from 'react'
const useEffectBadExample = () => {
useEffect(() => {
const clickHandler = e => console.log('e:', e)
document.getElementById('btn').addEventListener('click', clickHandler)
})
return <button id="btn">click me</button>
}
正確的做法是:
useEffect 的回調(diào)函數(shù)應(yīng)該返回一個函數(shù),用來解除綁定。
useEffect 應(yīng)該提供第二個參數(shù),為空數(shù)組,保證只會運(yùn)行一次。
import React, { useMemo } from 'react'
const UseEffectBadExample = () => {
useEffect(() => {
const clickHandler = e => console.log('e:', e)
document.getElementById('btn').addEventListener('click', clickHandler)
return () => document.getElementById('btn').removeEventListener('click', clickHandler)
}, [])
return <button id="btn">click me</button>
}
6. useState 使用不當(dāng)
useState 同樣是 React 中使用率最高的兩個 Hook 之一。但是令很多人困惑的是,useState 可能并不會按照他的預(yù)期去工作。比如一個圖片壓縮組件:
function Compress() {
const [files, setFiles] = useState([])
const handleChange = (newFiles) => {
api(newFiles).then((res)=>{
const cloneFiles = [...files]// 這里的 file 始終是[]
cloneFiles.map(
// 一些邏輯...
)
setFiles(cloneFiles)
})
}
return <input type="upload" multiple onChange={handleChange}/>
}
應(yīng)該修改為:
function Compress() {
const [files, setFiles] = useState([])
const handleChange = (newFiles) => {
api(newFiles).then((res)=>{
setFiles((oldFiles) => {
const cloneFiles = [...files]// 這里的 file 是最新的
return cloneFiles.map(
// 一些邏輯...
)
})
})
}
return <input type="upload" multiple onChange={handleChange}/>
}
原因在于函數(shù)是基于當(dāng)前閉包使用的狀態(tài)。但是狀態(tài)更新后,會觸發(fā)渲染,并創(chuàng)建新的上下文,而不會影響之前的閉包。所以要讓程序按照預(yù)期執(zhí)行,必須使用下面的語法:
setFiles(oldFiles => [...oldFiles, ...res.data])
7. 布爾運(yùn)算符的錯誤使用
大多數(shù)情況下我們都會使用布爾值來控制頁面上某些元素的渲染,這是非常正常的事情。除此之外還有幾種其他方式來處理這種邏輯,最常用的是 && 運(yùn)算符,這也完全是 JavaScript 的功能,但有時它會有一些意想不到的后果。
const total = 0
const Component = () => total && `商品總數(shù): ${total}`
當(dāng)我們需要展示商品數(shù)量時,如果數(shù)量為 0,那么只會展示 0,而不是商品總數(shù):0。原因是 JavaScript 會將 0 所以最好不要依賴 JavaScript 的布爾值真假比較。正確的方式如下:
const total = 0
const Component = () => {
const hasItem = total > 0
return hasItem && `商品總數(shù): ${total}`
}
8. 到處使用三元表達(dá)式進(jìn)行條件渲染
三元表達(dá)式是一個非常簡潔的語法,在簡短的代碼中非常令人滿意。所以很多人喜歡在 React 中使用三元表達(dá)式來渲染組件。但是它的問題在于難以擴(kuò)展,在最簡單的三元表達(dá)式中沒什么問題,可一旦多個三元表達(dá)式組合到一起,就形成了難以閱讀的超大型組件。
import React, { useMemo } from 'react'
const VIPExample = ({ vipLevel }) => {
return (<div>
會員系統(tǒng)
{vipLevel === 0 ? (
<button>開通 VIP</button>
) : vipLevel === 1 ? (
<p>尊敬的青銅VIP,您的特權(quán)有3項:...</p>
) : vipLevel === 2 ? (
<p>...</p>
) : <p>...</p>}
</div>)
}
這種代碼沒有功能性上的錯誤,但是在可讀性方面做得很差。解決它的辦法有兩種。第一種是使用條件判斷代替三元表達(dá)式。
import React, { useMemo } from 'react'
const VIPDetail = (vipLevel) => {
if(vipLevel === 0) return <button>開通 VIP</button>
if(vipLevel === 1) return <p>尊敬的青銅VIP,您的特權(quán)有3項:...</p>
// ...
}
const VIPExample = ({ vipLevel }) => {
return (<div>
會員系統(tǒng)
{VIPDetail(vipLevel)}
</div>)
}
如果每個分支中的組件比較復(fù)雜,我們更進(jìn)一步,我們使用抽象來封裝組件。
import React, { useMemo } from 'react'
const VIPZeroDetail = ({ vipLevel }) => {
if(vipLevel !== 0) return null
return <button>開通 VIP</button>
}
const VIPOneDetail = ({ vipLevel }) => {
if(vipLevel !== 1) return null
return <p>尊敬的青銅VIP,您的特權(quán)有3項:...</p>
}
// ...
const VIP = ({ vipLevel }) => {
return <>
<VIPZeroDetail vipLevel={vipLevel} />
<VIPOneDetail vipLevel={vipLevel} />
<!-->...<-->
</>
}
const VIPExample = ({ vipLevel }) => {
return (<div>
會員系統(tǒng)
<VIP vipLevel={vipLevel} />
</div>)
}
大多數(shù)情況下使用條件判斷的方式就夠用了。使用抽象封裝組件的方式有個缺點(diǎn),就是組件太過于散亂,同步邏輯比較麻煩。
9. 不定義 propTypes 或者不解構(gòu) props
React 的大多數(shù)東西和 JavaScript 幾乎是一樣的。React 的 props 也只是 JavaScript 中的對象,這也就意味著我們可以在對象中傳遞許多不同的值,而組件很難知道它們。這樣組件在使用 props 時就變得比較麻煩。很多人喜歡這么訪問 props。
const Example = (props) => {
return <div>
<h1>{props.title}</h1>
<p>{props.content}</p>
</div>
}
在不使用 TypeScript 或者不定義 propsTypes 的情況下,我們可以隨意使用 props.xxx 的方式來訪問 props。為了解決這個問題,我們可以選擇使用 TypeScript 為組件的 props 聲明類型。如果你沒有使用 TypeScript,那么可以使用 propTypes。同時建議將 props 以解構(gòu)的方式使用。
const Example = ({ title, content }) => {
return <div>
<h1>{title}</h1>
<p>{content}</p>
</div>
}
Example.propTypes = {
title: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
}
這樣組件需要哪些 props,我們一目了然。而且當(dāng)我們試圖訪問 props 上面不存在的屬性時,會得到警告。
10. 不對大型應(yīng)用代碼進(jìn)行拆分
大型的應(yīng)用意味著包含大量的組件。這時我們應(yīng)該使用代碼拆分的方式將應(yīng)用分成多個 js 文件,在用到哪些文件時再去加載它們。這樣可以讓應(yīng)用的初始包體積很小,讓用戶啟動網(wǎng)頁的速度更快。react-loadable 是一個專門處理這件事的第三方庫,使用它我們可以很好的將組件進(jìn)行拆分。
import Loadable from 'react-loadable'
import Loading from 'loading'
const LoadableComponent = Loadable({
loader: () => import('./component'),
loading: Loading
})
export default () => <LoadableComponent />
總結(jié)
React 為我們提供了一個強(qiáng)大的開發(fā)生態(tài)及開發(fā)工具集,我們可以比過去更加輕易地創(chuàng)建 Web 應(yīng)用。不過,它是一套工具,是工具就可能會被濫用。只有按照預(yù)期去使用工具,并且以優(yōu)先使用 JavaScript 的方式,才能使我們創(chuàng)建出邏輯更清晰、功能更強(qiáng)大、性能更卓越的代碼。作為開發(fā)者,持續(xù)改進(jìn)我們的代碼,讓用戶用起來舒服,讓其他開發(fā)者讀起來舒服,是我們應(yīng)該努力的方向和目標(biāo)。我的這 10 條建議,可以作為你用好 React 的一個起點(diǎn),希望能夠幫你規(guī)避很多開發(fā)過程中容易出現(xiàn)的錯誤。
作者:代碼與野獸
原文:https://juejin.cn/post/7131270450918719519
歡迎關(guān)注微信公眾號 :深圳灣碼農(nóng)