三個你不需要getDerivedStateFromProps的時機

CHC1024
狗奴工程師
Published in
10 min readJun 1, 2020

前言

在React的渲染機制,有些生命週期成為影響效能的關鍵,不乏有常見的componentDidMountcomponentDidUpdate或是shouldComponentUpdate等等,都是在撰寫元件時很重要的關鍵道具。

在v16.3更新之後出現了一個新的週期 — getDerivedStateFromProps,翻譯作「繼承state」,簡稱gDSFP,他是除了render之外每次會出現在元件渲染過程中的週期。

那為什麼我會特別想開一篇來講這個新加入的週期呢?

首先,在React v16更新系列中,除了function hook之外,新引入的生命週期也是一大亮點,因為gDSFP是React第一個靜態方法週期。

依照官方的說法,gDSFP是為了替代componentWillReceiveProps,並且配合componentDidUpdate能夠保持原有的功能。但老實說,gDSFP的用處並不多,若是開發時太依賴它,反而可能造成元件的更新邏輯太過於複雜,導致難以維護的情形。因此,我想整理一些使用情境來說明「什麼時候你不需要getDerivedStateFromProps」,以及提供相對應的解決方法。

如果你對gDSFP的用法有任何疑惑,又不知道自己為何需要它的話,就跟著我一起看下去吧!

getDerivedStateFromProps想解決什麼問題?

引入一個新的功能或是技術必然是想解決一些問題,那gDSFP主要想解決什麼議題呢?

原先prop在更新時,會呼叫componentWillReceiveProps來讓你可以針對這些更動順勢改變state,並減少多餘的渲染。然而,因為開發者常常誤用它來執行一些非同步的程式,導致元件內部的不穩定。

在同一時期,React將內部的演算法做了大幅度的更動,將渲染更新分成三個階段:render、pre-commit以及commit。以致於大部分前綴為componentWill的生命週期的使用變得不合時宜,所以官方想找尋一個方法使元件在新的渲染模式下能夠更加穩定,減少bug的產生,最後gDSFP就因應而生。

因為要替代掉原先componentWillMountcomponentWillReceiveProps的地位,所以gDSFP在首次渲染或是後續更新上都會被呼叫到。

從這張圖中就能夠看出,無論是首次渲染或是之後的更新,gDSFP都幾乎是第一個被呼叫的週期:

那你可能會覺得:「光是提出一個新的週期去代替原先的兩個週期為什麼就能解決問題呢?」。

就像我之前講的,gDSFP是一個特別的週期,他是少數為靜態方法的週期,也就是說你完全沒辦法在這裡存取this這個變數,所以我們幾乎只能仰賴傳遞進來的參數,像是nextPropsprevState

想一下,假如我們無法存取this會發生什麼事?

沒錯,如果你有定義任何操作DOM的方法,在這裡是沒有辦法做任何呼叫,因為你必須通過this才能觸碰到DOM ref。除此之外,你也無法執行非同步請求,因為生命週期是不會等你請求完成才進行回傳,整個執行過程是同步的,況且在這裡也無法呼叫this.setState,所以任何非同步行為都變得無效。

簡單來說,gDSFP運用靜態方法的特性避免開發者存取this來呼叫setState或是其他instance method,讓他們更專注在檢查新的prop與舊的繼承state來決定是否更新state,如此一來可減低人們誤用的可能性。

但gDSFP還是存在一些問題,像是gDSFP在 v16.4 更新之後,無論元件是因為什麼而進行渲染更新(父元件更新、呼叫setState),都會無條件呼叫gDSFP,所以也會大大地增加元件維護上的難度,因此官方也積極要求開發者少用gDSFP

接下來就看看哪些情形你不需要gDSFP吧~

避免使用getDerivedStateFromProps的時機

一、重新計算某些資料

問題點

假設我們有一個List會依據使用者挑選的過濾方式將特定的項目挑出來,這裡將篩選結果稱做filteredList,並且假定這個過濾成本非常昂貴。

如果我們今天選擇使用gDSFP來實作,大概會長得像這樣:

透過比對之前的list與filterText的確可以避免任何更新就無腦計算一次的情形,但我們得要額外去記得上次的list與filterText的內容,要是list的內容相當龐大,而且prop不常出現變化的話,這可能會造成使用太多的資源在無謂的比較上。

解決方法

你可以選擇使用memoize的方式將先前的計算結果快取下來,像是比較著名的函式庫reselect以及memoize-one都是不錯的選擇。

通常我們在計算時不會動用到所有的prop,所以這些memoize函式庫都提供了類似hook有的dependency list,差別只在於他們的計算函式跟dependency list的參數是用同一份,而hook則是能分開設定。

二、重置state

問題點

假設寫了一個可以用email登入帳號的網頁,而且類似google登入的機制,可以幫你記錄之前登錄的帳號,方便下次直接點省下打字的時間。

為了單純展示功能,我寫了一個很陽春介面:

裡面我用gDSFP來實作切換帳號的部分,看起來操作上沒什麼問題,但事實上你會發現在EmailInput裡面存放著使用者的ID,一個input可能沒什麼,但假如今天有五個十個都需要重置的話,就變得冗贅。

當然,用一個表單form把這些input包起來,只將使用者ID傳給這個form也是可以的,然而最關鍵的點是「這些元件並不關心使用者ID是什麼」。

因為一個input,他的任務是接收使用者的輸入,他只需要將這些輸入傳到上層去,告知他們使用者輸入了什麼即可,並不需要知道這是哪位使用者,所以這個時候也不適合使用gDSFP

解決方法

一般元件可以分成兩大類:controlled跟uncontrolled。

controlled是指元件的資料來源大部分由父層掌管,也就是由prop來決定;至於uncontrolled的資料則是由內部state提供。

我們可以利用他們的這些特性來解決這項問題:

Controlled元件

將原先的state從EmailInput拉出來,抬升到SigninPage當中,並提供EmailInput一個能夠修改這個state的function。這樣做的好處是整個輸入結果都會由SigninPage掌控,並且當使用者切換帳號時,也能直接在SigninPage內部直接切換email,底層不會意識到發生切換帳號。

修改後的code會長這樣:

Uncontrolled元件

因為此時的EmailInput的state不會受到SigninPage的控制,所以我們必須找尋某種方法可以強制EmailInput在切換帳號時能夠被告知要「重置」email的內容。

通知重製的方法有兩種:設定key或是使用ref。

▶ 設定key [Recommended]

還記得React有個機制是如果創建元件時給予一個key值,當這個值改變時會導致整個元件消滅並再次掛載到頁面上嗎?

如果我們運用使用者的ID作為EmailInput的key值,那麼當切換帳號時,使用者ID便會更動,連帶EmailInput也會重新被加入到DOM Tree上,先前的所有紀錄也因此會全數消滅達到重置的效果。

修改後的code:

這麼做的優點有:

1. 假如重置成本太高,直接重新創建一個新的元件會更簡便快速。

2. 無須加入任何重置的程式碼,元件複雜度可降低。

但相反的也會有缺點:

1. 假如建立元件成本比重置來得高,反而會降低效能。

2. 不是每個元件都能夠找到唯一的key值(雖然這種情況不常見)

▶ 使用ref [替代方案]

若你覺得設定key值會來得更慢或只有部分的state需要重置,也可以考慮使用ref將元件內部的方法暴露給父元件使用。

像是這樣:

先在EmailInput裡面寫一個reset function,並在SigninPage中賦予它一個ref,如此一來只要想重置某個state時就呼叫該方法即可。

修改後的code:

總結來說,大部分的情況下你不會需要使用到getDerivedStateFromProps這個新的生命週期,因為使用情境很少且大多可以有更好的解決方案。而且身為React開發團隊一員的Dan神也曾在Twitter發表推文表示這名字這麼長就是不希望我們去使用它。

那到底什麼時候會需要gDSFP呢?在我看完官方的blog後得到一個結論:

當你的state需要依據某個prop的變化進行重置,並且這個state與元件具有高度相關性時,才可以使用gDSFP。

有個例子我覺得還蠻不錯的,有興趣的讀者可以去看看,該篇作者以比對前後url的值來決定isLoading是否重置。但老話一句:「不到最後關頭絕不使用gDSFP」。

欸?你不是說三個時機嗎,怎麼才兩個?

第三個其實已經講完,而且嚴格來說是第一個,就是最前面我們不是講到gDSFP不具有存取this的權限嗎,當時有提到因此gDSFP沒有辦法執行資料提取或是操作DOM,那個就是其中一個。而且比起後面兩個時機,無法fetch data跟play animation比較算是官方明定的限制,所以沒有再特別拿出來提。

總之,當你開發時如果遇到上面三種情境,請再思考一下是否真的需要使用gDSFP,是否存在更好的解決方法,以避免元件變得複雜。

後記

在寫這一篇的時候,我覺得自己每天都是行屍走肉般在過生活,因為三不五時就在思考React推出getDerivedStateFromProps的目的到底是什麼,但即便看完很多方的觀點後,還是找不到一個真的能夠代表它的案例,後來我就放棄尋找能夠使用gDSFP的時機,因為真的太少了,反而專注在何時不能用gDSFP,這也是為什麼標題會是「三個你不需要getDerivedStateFromProps的時機」。

另外,今天這一篇中我終於跑去codesandbox辦了一個帳號,之前都不知道原來能夠直接引入codesandbox的專案進來,所以都傻傻的使用圖片或是gist呈現程式碼。至於跟Codepen相比,codesandbox在建立React專案的方式更加直覺且簡易,加入dependency也相對快速。除此之外,它撰寫程式碼的模式跟VS Code幾乎一模一樣,並且能更改當點選分享連結過來時要先開啟哪個檔案,總之可以玩很多東西,所以我未來呈現React應該都會轉向使用codesandbox。

參考資料

React官方Blog

Dan神如何看待gDSFP

--

--