
作者:蘇木團隊:增長中心
前言:react hooks被越來越多的人認可,整個社群都以積極的态度去擁抱它。在最近的一段時間筆者也開始在一些項目中嘗試去使用react hooks。原本以為react hooks很簡單,和類元件差不多,看看api就能用起來了。結果在使用中遇到了各種各樣的坑,通過閱讀react hooks相關的文章發現react hooks和類元件有很多不同。由此,想和大家做一些分享。
如果要在項目中使用react hooks,強烈推薦先安裝<code>eslint-plugin-react-hooks</code>(由react官方釋出)。在很多時候,這個eslint插件在我們使用react hooks的過程中,會幫我們避免很多問題。
本文主要講以下内容:
函數式元件和類元件的不同
react hooks依賴數組的工作方式
如何在react hooks中擷取資料
一、函數式元件和類元件的不同react hooks由于是函數式元件,在異步操作或者使用usecallback、useeffect、usememo等api時會形成閉包。
先看一下以下例子。在點選了<code>展示現在的值</code>按鈕三秒後,會alert點選次數:
我們按照下面的步驟去操作:
點選<code>num</code>到3
點選<code>展示現在的值</code>按鈕
在定時器回調觸發之前,點選增加<code>num</code>到5。
可以猜一下alert會彈出什麼?
分割線
其最後彈出的資料是3。
為什麼會出現這樣的情況,最後的<code>num</code>不是應該是5嗎?
上面例子中,<code>num</code>僅是一個數字而已。它不是神奇的“data binding”, “watcher”, “proxy”,或者其他任何東西。它就是一個普通的數字像下面這個一樣:
我們元件第一次渲染的時候,從<code>usestate()</code>拿到<code>num</code>的初始值為0,當我們調用<code>setnum(1)</code>,react會再次渲染元件,這一次<code>num</code>是1。如此等等:
在我們更新狀态之後,react會重新渲染元件。每一次渲染都能拿到獨立的<code>num</code>狀态,這個狀态值是函數中的一個常量。
是以在<code>num</code>為3時,我們點選了<code>展示現在的值</code>按鈕,就相當于:
即便num的值被點選到了5。但是觸發點選事件時,捕獲到的<code>num</code>值為3。
上面的功能,我們嘗試用類元件實作一遍:
我們按照之前同樣的步驟去操作:
在定時器回調觸發之前,點選增加<code>num</code>到5
這一次彈出的資料是5。為什麼同樣的例子在類元件會有這樣的表現呢?我們可以仔細看一下handleclick方法:
這個類方法從this.state.num中讀取資料,在react中state是不可變的。然而,this是可變的。通過類元件的<code>this</code>,我們可以擷取到最新的state和props。是以如果在使用者再點選了<code>展示現在的值</code>按鈕的情況下我們對<code>點選</code>按鈕又點選了幾次,<code>this.state</code>将會改變。<code>handleclick</code>方法從一個“過于新”的<code>state</code>中得到了<code>num</code>。
這樣就引起了一個問題,如果說我們ui在概念上是目前應用狀态的一個函數,那麼事件處理程式和視覺輸出都應該是渲染結果的一部分。我們的事件處理程式應該有一個特定的props和state。然而在類元件中,我們通過<code>this.state</code>讀取的資料并不能保證其是一個特定的state。<code>handleclick</code>事件處理程式并沒有與任何一個特定的渲染綁定在一起。從上面的例子,我們可以看出react hooks在某一個特定渲染中state和props是與其相綁定的,然而類元件并不是。
二、react hooks依賴數組的工作方式在react hooks提供的很多api都有遵循依賴數組的工作方式,比如usecallback、useeffect、usememo等等。使用了這類api,其傳入的函數、資料等等都會被緩存。被緩存的内容其依賴的props、state等值就像上面的例子一樣都是“不變”的。隻有當依賴數組中的依賴發生變化,它才會被重新建立,得到最新的props、state。是以在用這類api時我們要特别注意,在依賴數組内一定要填入依賴的props、state等值。
這裡給大家舉一個反例:
<code>usecallback</code>本質上是添加了一層依賴檢查。當我們函數本身隻在需要的時候才改變。在上面的例子中,我們無論點選多少次<code>點選</code>按鈕,<code>num</code>的值始終為1。這是因為<code>usecallback</code>中的函數被緩存了,其依賴數組為空數組,傳入其中的函數會被一直緩存。<code>handleclick</code>其實一直都是:
即便函數再次更新,<code>num</code>的值變為1,但是react并不知道你的函數中依賴了<code>num</code>,需要去更新函數。
唯有在依賴數組中傳入了<code>num</code>,react才會知道你依賴了<code>num</code>,在<code>num</code>的值改變時,需要更新函數。
點選<code>點選</code>按鈕,num的值不斷增加。
(其實這些歸根究底,就是react hooks會形成閉包)
三、如何在react hooks中擷取資料在我們用習慣了類元件模式,我們在用react hooks中擷取資料時,一般剛開始大家都會這麼寫吧:
其實這樣是不推薦的一種模式,要記住effect外部的函數使用了哪些props和state很難。這也是為什麼通常你會想要在effect内部去聲明它所需要的函數。這樣就能容易的看出那個effect依賴了元件作用域中的哪些值:
但是如果你在不止一個地方用到了這個函數或者别的原因,你無法把一個函數移動到effect内部,還有一些其他辦法:
如果這函數不依賴state、props内部的變量。可以把這個函數移動到你的元件之外。這樣就不用其出現在依賴清單中了。
如果其不依賴state、props。但是依賴内部變量,可以将其在effect之外調用它,并讓effect依賴于它的傳回值。
萬不得已的情況下,你可以把函數加入effect的依賴項,但把它的定義包裹進<code>usecallback</code>。這就確定了它不随渲染而改變,除非它自身的依賴發生了改變。
另外一方面,業務一旦變的複雜,在react hooks中用類元件那種方式擷取資料也會有别的問題。我們做這樣一個假設,一個請求入參依賴于兩個狀态分别是query和id。然而id的值需要異步擷取(隻要擷取一次,就可以在這個元件解除安裝之前一直用),query的值從props傳入:
在這裡,當我們的依賴的<code>query</code>在異步擷取<code>id</code>期間變了,最後請求的入參,其<code>query</code>将會用之前的值。(引起這個問題的原因還是閉包,這裡就不再複述了)
對于從後端擷取資料,我們應該用react hooks的方式去擷取。這是一種關注資料流和同步思維的方式。對于剛才這個例子,我們可以這樣解決:
一方面這種方式可以讓我們的代碼更加清晰,一眼就能看明白擷取這個接口的資料依賴了哪些state、props,讓我們更多的去關注資料流的改變。另外一方面也避免了閉包可能會引起的問題。但是同步思維的方式也會有一些坑,比如這樣的場景,有一個清單,這個清單可以通過子元素的按鈕增加資料:
這種場景下,會一直加載資料,造成死循環。每次調用<code>fetchdata</code>函數會更新<code>list</code>,<code>list</code>更新後<code>fetchdata</code>函數就會被更新。<code>fetchdata</code>更新後<code>useeffect</code>會被調用,<code>useeffect</code>中又調用了<code>fetchdata</code>函數。<code>fetchdata</code>被調用導緻<code>list</code>更新...當出現這種根據前一個狀态更新狀态的時候,我們可以用usereducer去替換usestate:
react會保證<code>dispatch</code>在元件的聲明周期内保持不變。是以上面的例子中不需要依賴<code>dispatch</code>。
用了<code>usereducer</code>我們就可以移除<code>list</code>依賴。不會再出現死循環的情況。通過dispatch了一個action來描述發生了什麼。這使得我們的<code>fetchdata</code>函數和<code>list</code>狀态解耦。我們的<code>fetchdata</code>函數不再關心怎麼更新狀态,它隻負責告訴我們發生了什麼。更新的邏輯全都交由reducer去統一處理。
(我們使用函數式更新也能解決這個問題,但是更推薦使用usereducer)在某些場景下<code>usereducer</code>會比usestate更适用。例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴于之前的state等。并且,使用 <code>usereducer</code> 還能給那些會觸發深更新的元件做性能優化,因為你可以向子元件傳遞 <code>dispatch</code> 而不是回調函數。
如果大家遇到其它的一些複雜場景,用上面介紹的方法無法解決。那就試試用useref吧。文章如有疏漏、錯誤歡迎批評指正。