[譯]5個技巧:避免React Hooks 常見問題

[譯]5個技巧:避免React Hooks 常見問題

原文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

問題1:在理解以前就急於使用Hooks

React Hooks官方文檔 寫得很是詳盡,我強烈建議你在使用 Hooks 以前,把官方文檔 通讀 一遍,尤爲是 FAQ 部分,裏面包含了不少實際開發中會遇到的問題及解決辦法。給你本身一兩個小時,通讀一下官方文檔吧,這將對你理解 Hooks 有很大的幫助,而且在未來的實際開發中幫你節省不少(找bug和改bug的)時間。git

與此同時,建議你也看一看 Sophie, Dan 和 Ryan介紹Hooks的分享github

第一個問題的解決辦法:仔細研讀官方文檔以及FAQ 📚數組

問題2:不使用(或忽視)React Hooks的ESLint插件

在 React Hooks發佈的同時,eslint-plugin-react-hooks 這個 ESLint的插件也發佈了。這個插件包含兩個校驗規則:rules of hooksexhaustive 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 ,可是因爲咱們在 DogInfouseEffect 依賴項中,寫的空數組,致使這個 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 ** 。

問題3:(錯誤地)從組件生命週期角度來思考Hooks

在 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 裏處理了 propstate 的變化,可是卻忘記了取消掉上一次變化所引發的反作用。舉個栗子,你發起了某次 HTTP 請求,可是在 HTTP 完成以前,組件的某個 prop 或 state 發生了變化,那麼你一般應該取消掉這個HTTP請求。

在使用 React Hooks 的場景下,你仍然須要思考你的反作用在什麼時機執行,可是你不用再糾結反作用是在哪一個生命週期裏執行,你思考的是,怎麼保持反作用的結果和組件的狀態同步。要理解這個點,須要付出一些努力,可是你一旦理解到了,你將避免不少的bug。

所以,你能夠給 useEffect 的依賴項設置爲 空數組 的唯一理由,是它確實沒有依賴任何外部變量,而 不是 你認爲這個反作用只須要在組件mount的時候執行一次。

第三個問題解決辦法:不要以組件生命週期的方式來思考 React Hooks,應該是思考,若是讓你的反作用和組件狀態保持一致

問題4:過於擔憂性能

一些開發者看到下面的代碼時,他們嚇壞了:

function MyComponent() {
  function handleClick() {
    console.log('clicked some other component')
  }
  return <SomeOtherComponent onClick={handleClick} /> } 複製代碼

他們一般由於下面2個緣由而感到擔心:

  1. 咱們在 MyComponent 內部定義了函數handleClick,這意味着,每次 MyComponent render時,都會從新定義一個不一樣的handleClick
  2. 每次render,咱們都將一個新的handleClick傳給了 SomeOtherComponent,這意味着咱們不能經過 React.memoReact.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.memoReact.useMemo以及 React.useCallback。你能夠從個人這篇博客,瞭解到 useMemo和useCallback 。注意:有的時候,你採起了性能優化措施以後,你的APP反而更卡頓了……所以,務必在性能優化的先後,作好性能檢測對比。

同時記住,production版本的React性能比development版本高不少

第四個問題解決辦法:記住一點,React原本就執行很快,不要過早的擔憂或者優化你的性能

問題5:對Hooks的測試過於重視

我注意到有些開發者擔憂,若是他們講組件遷移到 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 問題的解決辦法:

  1. 仔細研讀官方 Hooks 文檔,以及 FAQ 部分
  2. 安裝、使用而且遵照 eslint-plugin-react-hooks 這個 ESLint 插件
  3. 忘掉組件生命週期的思考方式吧。正確的姿式:怎麼保持反作用和組件狀態的同步。
  4. React 自己執行很快,在過早的性能優化以前,必定作好相關的知識點調研
  5. 避免測試組件的實現細節,應該關注組件的輸入和輸出
相關文章
相關標籤/搜索