原文: kentcdodds.com/blog/react-…javascript
在這篇文章裏,咱們來探索下 React Hooks 的常見問題,以及怎麼來避免這些問題。html
React Hooks 是在 2018年10月提出 ,而且在2019年2月 發佈 。自從 React Hooks 發佈之後,不少開發者都在項目中使用了Hooks,由於Hooks確實在很大程度上簡化了咱們對組件 state
和 反作用
的管理。java
毫無疑問,React Hooks 目前是react生態中一個熱點,愈來愈多的開發者以及開源庫,都引入了Hooks。儘管React Hooks 如今收到追捧,可是它的引入,也須要開發者改變本身對於組件生命週期、state以及反作用的思考方式;若是你沒有很好的理解 React Hooks,盲目的使用它將會給你帶來一些意想不到的bug。OK,接下來咱們就來看看使用Hooks可能有哪些坑,以及怎麼改變咱們的思考方式來避免這些坑。react
React Hooks官方文檔 寫得很是詳盡,我強烈建議你在使用 Hooks 以前,把官方文檔 通讀 一遍,尤爲是 FAQ 部分,裏面包含了不少實際開發中會遇到的問題及解決辦法。給你本身一兩個小時,通讀一下官方文檔吧,這將對你理解 Hooks 有很大的幫助,而且在未來的實際開發中幫你節省不少(找bug和改bug的)時間。git
與此同時,建議你也看一看 Sophie, Dan 和 Ryan介紹Hooks的分享 。github
第一個問題的解決辦法:仔細研讀官方文檔以及FAQ 📚數組
在 React Hooks發佈的同時,eslint-plugin-react-hooks 這個 ESLint
的插件也發佈了。這個插件包含兩個校驗規則:rules of hooks
和 exhaustive deps
。默認的推薦配置,是將 rules of hooks
設置爲 error
級別,將 exhaustive deps
設置爲 warning
級別。性能優化
我強烈建議你安裝、使用並遵照這兩條規則。它不只僅能幫你發現容易被忽略的bug,在這個過程當中,還能教你一些代碼和hooks的知識,固然了,還有它提供的代碼自動修復功能,超cool。app
在我和不少開發者交流中,發現不少人開發者對 exhaustive deps
這條規則感到困惑。所以,我寫了一個簡單的demo,來展現若是忽略了這條規則,將會致使什麼bug。ide
假設咱們有2個頁面:一個是 狗狗🐶 列表頁 List
,展現一系列狗狗的名字;一個是某一隻狗狗的詳情頁Detail
。在列表頁上,點擊某個狗狗的名字,就會打開對應狗狗的詳情頁。
OK,在狗狗詳情頁上,咱們有一個展現狗狗詳情的組件 DogInfo
,它接收狗狗的 dogId
,而且根據 dogId
請求API獲取對應的詳情:
function DogInfo({dogId}) {
const [dog, setDog] = useState(null)
// imagine you also have loading/error states. omitting to save space...
useEffect(() => {
getDog(dogId).then(d => setDog(d))
}, []) // 😱
return <div>{/* render the dog info here */}</div>
}
複製代碼
上面的代碼裏,咱們 useEffect
的依賴列表是一個空數組,由於咱們只但願在組件 mount
的時候纔去發起一次請求。到目前爲止,這段代碼沒什麼問題。如今,假設咱們的狗狗詳情頁UI有了一點改動,增長了一個 「相關狗狗」的列表。這時咱們的代碼就有bug了,點擊 「相關狗狗」 列表中的某一隻,咱們的 DogInfo
組件並不會更新到對應的狗狗詳情,儘管 DogInfo
組件已經從新 render
了。
如今的狀況是,點擊 「相關狗狗」 列表中的某一項,觸發了詳情頁的從新 render
,而且會把點擊的狗狗 dogId
傳給 DogInfo
,可是因爲咱們在 DogInfo
的 useEffect
依賴項中,寫的空數組,致使這個 useEffect
不會從新執行。
嗯,下面是修改以後的代碼:
function DogInfo({dogId}) {
const [dog, setDog] = useState(null)
// imagine you also have loading/error states. omitting to save space...
useEffect(() => {
getDog(dogId).then(d => setDog(d))
}, [dogId]) // ✅
return <div>{/* render the dog info here */}</div>
}
複製代碼
經過這個栗子,咱們能夠得出這個關鍵結論:若是 useEffect
的某個依賴項真的永遠不會改變,那把這個依賴項添加到 uesEffect
的依賴數組裏,也沒有任何問題。同時,若是你認爲某個依賴項不會改變,但實際上這個依賴項卻變了,這正好幫助你發現了代碼裏的bug。
和這個例子相比,還有不少其餘的場景更難辨別和分析,好比,你在 useEffect
裏調用了某個函數(函數定義在 useEffect 外面),可是卻 沒有 在依賴項裏添加這個函數,那麼代碼極可能有bug了。相信我,每次在我忽略了這條規則以後,我都會後悔當初爲何沒有遵照規則。
請注意,受限於 ESLint
在代碼靜態分析的一些限制,這條規則 (exhaustive deps
)有時候不能正確的分析出你代碼中的問題。可能這就是它爲何默認設置級別是 warning
而不是 error
的緣由吧。當它不會正確的分析你的代碼時,它會給出你一些warning信息,在這種狀況下,我建議你稍微重構下你的代碼,來保證能正確的被分析。若是重構代碼以後,依然不能被正確的分析,那麼可能局部的關閉這條規則,也是一個辦法吧,爲了能繼續coding而不至於延期。
第二個問題的解決辦法:**安裝、使用而且遵照 ESLint ** 。
在 React Hooks 出現以前,咱們能夠在類組件裏,經過內置的組件生命週期方法,來告訴react,何時,它應該作什麼操做:
class LifecycleComponent extends React.Component {
constructor() {
// initialize component instance
}
componentDidMount() {
// run this code when the component is first added to the page
}
componentDidUpdate(prevProps, prevState) {
// run this code when the component is updated on the page
}
componentWillUnmount() {
// run this code when the component is removed from the page
}
render() {
// call me anytime you need some react elements...
}
}
複製代碼
在 Hooks 發佈以後,像上面這些寫類組件一樣沒問題(在可預見的未來,這樣寫也沒有任何問題),這種類組件的方式已經存在了許多年。Hooks 帶來了一系列的好處,其中我最喜歡的一個好處是(useEffect
),Hooks 使得組件更加的符合聲明式 語法。有了 Hooks,咱們能夠不用去分辨「某個操做應該在組件的哪個生命週期執行」,而是更加直觀的告訴 React,「當哪些變化發生時,我但願執行對應的操做」。
所以,如今咱們的代碼長這樣:
function HookComponent() {
React.useEffect(() => {
// This side effect code is here to synchronize the state of the world
// with the state of this component.
return function cleanup() {
// And I need to cleanup the previous side-effect before running a new one
}
// So I need this side-effect and it's cleanup to be re-run...
}, [when, any, ofThese, change])
React.useEffect(() => {
// this side effect will re-run on every single time this component is
// re-rendered to make sure that what it does is never stale.
})
React.useEffect(() => {
// this side effect can never get stale because
// it legitimately has no dependencies
}, [])
return /* some beautiful react elements */
}
複製代碼
Ryan Florence 從另外一個角度來解釋思考方式上的變化 。
我喜歡這個特性(useEffect)的一個主要緣由是,它幫助我避免了不少bug。在過去基於類組件的開發過程當中,我發現引入bug的不少情形,都是我忘記了在 componentDidUpdate
裏處理某個 prop
或者 state
的變化;另外一種狀況是,我在 componentDidUpdate
裏處理了 prop
或 state
的變化,可是卻忘記了取消掉上一次變化所引發的反作用。舉個栗子,你發起了某次 HTTP 請求,可是在 HTTP 完成以前,組件的某個 prop 或 state 發生了變化,那麼你一般應該取消掉這個HTTP請求。
在使用 React Hooks 的場景下,你仍然須要思考你的反作用在什麼時機執行,可是你不用再糾結反作用是在哪一個生命週期裏執行,你思考的是,怎麼保持反作用的結果和組件的狀態同步。要理解這個點,須要付出一些努力,可是你一旦理解到了,你將避免不少的bug。
所以,你能夠給 useEffect
的依賴項設置爲 空數組 的唯一理由,是它確實沒有依賴任何外部變量,而 不是 你認爲這個反作用只須要在組件mount的時候執行一次。
第三個問題解決辦法:不要以組件生命週期的方式來思考 React Hooks,應該是思考,若是讓你的反作用和組件狀態保持一致
一些開發者看到下面的代碼時,他們嚇壞了:
function MyComponent() {
function handleClick() {
console.log('clicked some other component')
}
return <SomeOtherComponent onClick={handleClick} /> } 複製代碼
他們一般由於下面2個緣由而感到擔心:
MyComponent
內部定義了函數handleClick
,這意味着,每次 MyComponent
render時,都會從新定義一個不一樣的handleClick
handleClick
傳給了 SomeOtherComponent
,這意味着咱們不能經過 React.memo
,React.PureComponent
或者 shouldComponentUpdate
來優化 SomeOtherComponent
的性能,這會引發SomeOtherComponent
許多沒必要要的從新render針對第一個問題,JavaScript引擎(即便是在很低端的手機上)定義一個新函數的執行是很是快的。你基本上不會遇到因爲重複定義函數而致使你的APP性能低下。
第二個問題來說,沒必要要的重複render,也不是必定會引發性能問題。僅僅由於組件從新render了,並不表明實際的DOM會被修改,一般修改DOM纔是慢的地方。React 在性能優化方面作的很是好,一般你沒有必要爲了提高性能去引入一些額外的工做。
若是這些額外的重複render致使你的APP慢,首先應該明確爲何重複render會這麼慢。若是一次render自己都很慢,致使額外的重複render引發APP卡頓,那麼即便你避免了額外的重複render,你極可能仍然面臨性能問題。當你修復了致使render慢的緣由以後,你或許會發現,那些重複的render也不會引發APP卡頓了。
若是你真的確認,是額外的重複render致使了APP性能問題,那麼你可使用 React 內置的一些性能優化 API,好比 React.memo
,React.useMemo
以及 React.useCallback
。你能夠從個人這篇博客,瞭解到 useMemo和useCallback 。注意:有的時候,你採起了性能優化措施以後,你的APP反而更卡頓了……所以,務必在性能優化的先後,作好性能檢測對比。
同時記住,production版本的React性能比development版本高不少 。
第四個問題解決辦法:記住一點,React原本就執行很快,不要過早的擔憂或者優化你的性能 。
我注意到有些開發者擔憂,若是他們講組件遷移到 React Hooks,他們須要重寫對應的全部測試代碼。根據你的測試代碼實現方式,這個擔心可能有道理,也可能沒有道理。
引用我本身文章 使用React Hooks,測試代碼怎麼辦? ,若是你的測試代碼長這樣:
test('setOpenIndex sets the open index state properly', () => {
// using enzyme
const wrapper = mount(<Accordion items={[]} />) expect(wrapper.state('openIndex')).toBe(0) wrapper.instance().setOpenIndex(1) expect(wrapper.state('openIndex')).toBe(1) }) 複製代碼
若是是這種狀況,那麼你正好藉着重寫測試代碼的機會,去優化這些測試代碼。毫無疑問,你應該廢棄掉上面這樣的代碼,改爲下面這樣的:
test('can open accordion items to see the contents', () => {
const hats = {title: 'Favorite Hats', contents: 'Fedoras are classy'}
const footware = {
title: 'Favorite Footware',
contents: 'Flipflops are the best',
}
const items = [hats, footware]
// using React Testing Library
const {getByText, queryByText} = render(<Accordion items={items} />) expect(getByText(hats.contents)).toBeInTheDocument() expect(queryByText(footware.contents)).toBeNull() fireEvent.click(getByText(footware.title)) expect(getByText(footware.contents)).toBeInTheDocument() expect(queryByText(hats.contents)).toBeNull() }) 複製代碼
這兩份測試代碼的關鍵區別是,舊的代碼是在測試 組件的具體實現,新的測試卻不是這樣。無論組件是基於類的實現方式,仍是基於 Hooks 的方式,都是組件內部的具體實現細節。所以,若是你的測試代碼,會放到到被測試組件的一些具體實現細節,(好比 .state()
或者 .instance()
),那麼將組件重構爲 Hooks 版本,確實會讓你的測試代碼失效。
可是使用你組件的開發者,是不關心你的組件是基於類實現,仍是基於 Hooks 實現。他們只關心你的組件可以正確的實現業務邏輯,或者說只關心你的組件渲染到屏幕上的內容。所以,若是你的測試代碼,是檢查組件渲染到屏幕上的內容,那麼無論你的組件是基於類仍是Hooks實現,都不影響測試代碼的運行。
你能夠從這兩篇文章瞭解更多關於測試方面的內容:對實現細節的測試 和 Avoid the Test User 。
OK,解決這個問題的辦法是:避免去測試組件的實現細節 。
說了這麼多,總結起來就是下面這些建議,幫你避免常見的 Hooks 問題的解決辦法: