關於React這套網頁框架,是從16.8正式支援Hooks,不再建議使用類別,而是使用函式來定義元件。換言之,在函式的開頭,我們可以透過鉤子「勾取」必要的狀態與副作用邏輯,到了後續的流程中,再進一步定義無狀態的使用者介面(UI)呈現邏輯。

React最想要的是?

在先前專欄〈高效的虛擬DOM?〉中,我曾經談過,React的核心設計就是「狀態一有變動,就用新畫面取代舊畫面」,React鼓勵使用無狀態元件(Stateless component),具體而言,就是本身沒有state特性,也不根據自身狀態建立UI的元件,這類元件透過props的資訊建立UI,也就是使用JSX描述UI,並從元件的render方法傳回。

由於元件僅透過props的資訊來建立UI,如果透過類別元件(Class component)來定義,就顯得小題大作。

而React其實很早就支援了函式元件(Function component),也就是使用props->UI的函式形式來定義元件。例如:

function Welcome(props) {
return <div>Hello, {props.name}</div>;
}

無狀態、無副作用的純函式(Pure function)是React最想要的元件定義方式,當元件以純函式表示時,就可以善用函數式設計,將函式組合為想要的功能。

然而,由純函式所定義出來的元件,並不支援componentWillMount等生命周期方法,也因為函式元件沒有狀態,必定在某個地方有副作用,像是:取得、變更狀態,將狀態資訊轉換為props、傳遞給函式元件,因而純函式元件目前無法完全取代類別元件。

重用類別元件邏輯

為了能夠管理狀態、副作用,以及支援元件的componentWillMount等生命周期方法,React從最早使用React.createClass,到後來的ES6支援類別語法,都是使用類別的概念來定義元件,React.Component幫開發者們定義了必要的方法與特性,接著,開發者繼承React.Component、定義this.state、使用setState等方法等,來定義生命周期、狀態等邏輯,最後透過render來傳回UI描述。

然而,定義在不同類別中的生命周期與狀態邏輯,可能出現相似甚至重複的程式碼,React早期還在使用React.createClass時,曾透過Mixin來解決,卻容易發生名稱衝突、耦合性高等問題,因而被認為是有害的作法,到了現在,運用ES6類別語法下,自然也不再能使用Mixin。

由於ES6類別語法本質上是個函式,有些開發者想到:可以將類別元件傳入函式,該函式傳回新的類別元件,而這類函式稱為高階元件(Higher-Order Component)。

在概念上,它們與可接受函式並傳回函式的高階函式相同。而在本質上,高階元件傳回的部分是個包裹器,傳入的元件藉此可重用包裹器中定義的邏輯(可參考〈Higher-Order Components〉(bit.ly/36tAY3T))。

另一個重用類別元件邏輯的方式是Render Props,開發者可在類別元件的JSX,定義函式特性(通常命名為render),在類別元件具有可重用的邏輯,並透過props取得該函式,將必要的資料傳入,以取得函式傳回的UI描述,這部份可參考〈Render Props〉(bit.ly/2JQ4CGK)。

無論是高階元件或Render Props,本質上都是以組合來代替繼承,也確實可以解決類別元件邏輯重用的問題,然而,實現上需要大量的程式碼,而且各自有容易形成嵌套、回呼堆疊的問題,在UI元件複雜的情況下,維護上就會發生困難。

把狀態(副作用)勾進來!

React後來推出Hooks,由框架直接支援的狀態(副作用)邏輯重用方式,開發者一開始依照React的期許,使用純函式開始設計元件,在需要狀態或副作用時,透過Hooks勾進來:

function Welcome(props) {
const [width, setWidth] = useState(100);
return <div width={width}>Hello, {props.name}</div>;
}

useState就是Hooks,也就是React提供的鉤子,具體來說,就是個不純的函式,以useState來說,是用來勾取狀態,100是狀態預設值,傳回值是陣列,第一個元素是最新的狀態值,第二個是改變狀態的方法,通常結合事件使用,若透過該方法設定新的狀態,就會重新呼叫Welcome,傳回新的UI描述。

單看Hooks的使用方式,我們所得到的第一印象,是它隱藏了類別元件中this.state、setState等細節,使得(不純的)函式元件比類別元件簡潔,這確實也是目的之一;然而,更重要的是React規定,狀態的勾入必須在函式的開頭定義──關於這部份,我們可參考〈Rules of Hooks〉(bit.ly/32dmdP7),之後,再根據勾入的狀態、props等,以無副作用的方式來定義UI描述。

在Hooks的規則下,函式就有了分界──界線以上的部分是具有副作用的邏輯,以下的部分是無副作用的元件邏輯。

這樣的模式,令函式元件從純函式演進為不純的元件時,可以是個漸進、易於重構的過程,而不像以前,須大費周章地定義類別、重新搬移相關邏輯至對應的位置。

React的鉤子,在慣例上,是以use名稱開頭,React目前也提供了幾個常用的鉤子。例如,useContext用來支援基於Render Props的API,令實現Render Props得以簡化;useReducer用來支援Redux架構,可以勾取狀態與dispatch函式;useEffect用來實現具有副作用的流程,在類別元件中放在componentDidMount等生命周期方法中的程式碼,可以放在useEffect中實現。

事實上,React的鉤子,就是封裝了狀態、副作用邏輯的函式,如果不純的函式元件當中,有清楚的界線,對於界線以上的副作用邏輯,若發現多個元件間有相似或重複流程,可以自行封裝為新的鉤子,這就解決了使用類別元件時不易重用副作用邏輯的問題。

畫清「純」與「不純」的界線

關於Hooks的規則,令人聯想到不少開發者對純函數式語言的誤解。

一般而言,純函數式語言中並非沒有副作用,而是純函式與副作用的函式之間,有著明確的分野,在含有鉤子的函式中,狀態、副作用等的勾取,以及與純函式元件間,也有要有明確的界線。

Hooks的寫法經常被用來與類別元件相比較,以突顯採用Hooks風格的簡潔,然而Hooks其實是個新的思考方式,使用上不用具備類別元件的知識,過去對React熟悉的開發者,反而要忘了類別元件的使用方式,別試圖在學習Hooks的過程中,逐一比對兩者的差別。

重要的是,應遵照React的期望,以撰寫純函式元件作為開始,必要的時候再定義不純的函式,於開頭使用適當的鉤子,之後交由純函式元件來產生UI。如此一來,不單只有函式元件,具有副作用的鉤子也有機會封裝而獲得重用。

專欄作者

熱門新聞

Advertisement