React Hooks 是什麼?能吃嗎?

Mars Li
RD.TW
Published in
11 min readMar 12, 2020

React 在 v16.8 版後新增了全新的 API: React Hooks 究竟他有什麼特別的呢?這篇文章可能需要你有開發過 React 才比較好理解兩者的差異,當然我也會用通俗的話來講解。

為什麼要有 Hooks?

這邊官方給的理由是:

  1. Wrapper Hell:React 沒有提供一個方法把可重複利用的行為附加到 component 上,我們常常會利用 props render、HoC(higher-order components)、provider 等等的行為來去封裝我們的 component,這常常造成我們的程式碼內有很多的巢狀結構 (註1)
  2. 生命週期中參雜不相干的邏輯:因為有太多且相似的生命週期,造成常常我們會為了方便將不相干的邏輯放在不該放的生命週期內,讓組件變得難以理解,不要說維護的人看不懂,你自己過一段時間後也看不懂,還以為是別人寫的。
  3. Class 讓你跟電腦都很困惑:Class 裡面常常宣告 function 時要 bind this,雖然有 Arrow Function 可以解決但依舊會常常讓人搞混。另外 class component 也不利於 AOT compile、minify 及 hot loading
註1: Wrapper Hell

其實在以前就有 Function Component 的寫法了

function CustomBtn(props) {
return (<button>Hello {props.name}</button>)
}

但是他是無狀態無生命週期的,所以以前的 function component 並不能取代 class component,所以在 2018 的 React conf 就推出了 React Hooks,官方也有提到 hooks 不是要馬上取代 class,而是完全相容現有程式碼,以逐步取代的方式。

Class Component vs Function Component

這邊我們先回顧一下以前我們開發 React class 組件都是怎麼開發的。

下面我寫了一個範例,同一個功能的組件用 Class 跟 Hooks 的寫法,大家可以看看差異:

React Hooks 當初的用意就是希望組件應該是簡潔的,不應該把多種邏輯耦合在一起,增加維護的難度,所以當我們寫組件時的邏輯更簡潔清晰將會降低我們重構跟維護的成本。

Hooks 的中文意思是鉤子,意指 function component 應該是簡潔乾淨的,當需要某個功能時再把它勾進來用就好

大家看了上面的範例可能大概會覺得,喔 ~ 好像程式碼少很多,但那個 useState、useEffect 是幹嘛的?

這邊來跟大家解釋一下比較常用的幾個 Hooks:

useState

一個組件的靈魂應該就是狀態吧,一個組件除了外觀以外最重要的就是裡面的數據,跟人一樣不能只是一個花瓶(雖然還是有純 UI 的 Component、請忽略我的幹話),我們先來看看以前我們都是怎麼在組件內去使用狀態的。

狀態跟斯斯一樣有分兩種(props、state),通俗一點解釋的話就是,props 是你爸給你的,state 是你自己的

class Child extends React.Component {

constructor(props) {
super();
this.state = {
money: 0 // 本來我只有 0 元
};
}

componentDidMount () {
this.setState({
money: this.props.money // 爸爸給我的錢,放在我口袋裡備用
})
}

render () {
const { money } = this.state; // 從口袋裡拿出錢
return (
<div>{money}</div>
)
}
}
// 爸爸給你10塊
<Child money={10} />

React 內的數據流都是單向的,所以通常都是由 Parent 往下傳遞給 Child,然後 Child 拿到後可以在生命週期內做各種處理後做展示。

在傳統的做法內都是透過 constructor 這個建構函式去初始化狀態的 key、value,然後在生命週期內去改變狀態的值,最後在 render 時展示。

而在 Hooks 內也是同樣的邏輯,Hooks 則是用 useState 這個鉤子函式去處理 state:

function Child(props) {
// 爸爸給我的錢,放在我口袋裡備用
// 直接宣告 setMoney 函式,要改變狀態可以直接使用
const [money, setMoney] = useState(props.money)
return (
<div>{money}</div>
)
}

有沒有覺得這個 Child 瞬間瘦身很多,沒錯這就是 Hooks 的魔力啊,比直銷的瘦身產品還好用。

useEffect

有了狀態再來當然就是組件的生命週期,生命週期比較好解釋,例如你一天起床時、吃飯時、上大號時、睡覺時等等的行為,class component 比較常用的生命週期有:

React Lifecycle

而在 Hooks 內所對應的生命週期為

React hooks lifecycle

Effect 指的是副作用(side effect),什麼是副作用呢?也就是當你需要額外的功能輔助你來處理、取得資料的時候,你就把這些邏輯放到 useEffect 內,像是 fetchData、event handler binding,而在 Hooks 內 useEffect 本身就代表了三種生命週期,分別是 componentDidMount、componentDidUpdate、componentWillUnmount,我們直接來看程式碼:

// 只有第一次會執行
useEffect(() => {
console.log('componentDidMount')
}, [])

// 每次都會執行
useEffect(() => {
console.log('componentDidUpdate')
})

// 只有 count 這個變數有變化才會執行
useEffect(() => {
}, [count])

// 利用回傳函數來取消 side effect
useEffect(() => {
return () => { console.log('componentWillUnmount') }
})

useEffect 總共有兩個入參,第一個是執行函式,也就是每次 render 完會觸發的函式,第二個參數是一個陣列,是用來控制該 effect 該不該被觸發的,像是上面的範例,我如果傳一個空陣列,那他只會觸發一次,反之如果不傳就會每次執行,如果傳入變數的話就是當這些變數有改變時才會觸發執行。另外我們也會透過回傳函數去做取消 side effect 的動作。

我們可以看到每個 effect 都是拆開來的,這有什麼好處呢?

以往我們總是把所有邏輯全部寫在 componentDidMount 內,這造成我們程式碼難以閱讀,透過 useEffect 天然的拆分,我們的邏輯也會更清晰。

useLayoutEffect

useLayoutEffect 跟 useEffect 使用方法一樣,只是 useEffect 是非同步執行,他並不會阻塞瀏覽器渲染流程,而且會在瀏覽器渲染完後觸發(reflow、repaint 後觸發)。

而 useLayoutEffect 會在瀏覽器渲染前執行,執行完後才會去做渲染的動作,所以官方建議通常都先用 useEffect 來處理副作用,真的需要操作到 DOM 的話才用到 useLayoutEffect。

useMemo

這個 hooks 被設計來避免重複且複雜的計算,可以理解為他會幫你把上次的計算結果緩存起來,下次直接拿來使用,我們來看範例:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

我們可以看到第一個參數為計算的函數,第二個是判斷的依據,跟 useEffect 一樣傳入一個陣列,如果我們陣列內的值(此範例是入參 a、b)沒有變的話,那 computeExpensiveValue 將不會執行,會直接拿上次的結果來用,但是如果你的 function 不是會牽扯到大量計算的話就不需要用 useMemo。

你以為 useMemo 只能拿來做 function 的計算嗎?

開發時常常遇到 parent 內的 state 有改變造成 render 的時候,底下的 child 也會重新渲染,在以前我們會利用 componentDidUpdate、pure component 等等手段來防止 child 渲染,現在我們可以透過 useMemo、React.memo 來做到,下面就直接上官方的程式碼:

function Parent({ a, b }) {
// 只會在 `a` 改變時 re-render:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// 只會在 `b` 改變時 re-render:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
}

React.memo

import React, { memo } from 'react';
const Demo = React.memo(
(props) => <div>{props.children}</div>,
(prevProps, nextProps) => {
// true: 不重新渲染
// false: 重新渲染
return false;
}
);

React.memo 第一個入參為組件,第二個入參為判斷函式,他會傳入上一次的 props (prevProps)跟下一次的 props (nextProps),你可以自己去判斷 props 然後透過回傳布林值告訴 React 這個組件需不需要渲染。

useCallback

useCallback 跟 useMemo 類似,你可以把他想像成,useMemo 是用來緩存函數結果,useCallback 是用來緩存 function 的記憶體位置,為什麼我們會需要用到 useCallback 呢?我們先來看一下下面這段程式碼

function Comp(props) {
const clickHandler = () => {
// do something
};
return (
<Button onClick={clickHandler}>CLICK ME</Button>
);
}

先講一下結論,這個 component 每次渲染都會造成 Button 這個組件重新渲染,你可能會說 Button 本身又沒有傳入 props 跟上次一樣怎麼會造成渲染呢?原因就是 onClick 的事件 clickHandler,因為 Comp 每次重新渲染時 clickHandler 都是不同的實例,所以他都會當成不同的 props 造成重新渲染,所以這時候我們會利用 useCallback 讓他緩存這個 callback 的記憶體位置來避免重新渲染

useCallback(fn, deps) // useCallback 的定義

第一個參數就是函式,第二個參數跟 useMemo 一樣都是判斷要不要變更的的依據。

useRef

如果我們需要在 React 內獲取到該 component 的真實節點的話就可以使用 useRef

import React, { useRef } from 'react'function useRefExample() { 
const rootRef = useRef(null);
const getRootDOM = () => {
console.log(rootRef.current); // Got it!
}

return (
<div ref={rootRef}>
<button onClick={getRootDOM}>GetRootDOM</button>
</div>
)
}

最常用的 hooks 都在上面了,希望大家看完之後能大概了解 hooks 的起源跟用法,趕快用在實務上吧!當然還有很多沒有講的,像是實作原理,或是自己做一個 hooks 該怎麼下手,有時間的話我會再出一篇。

有時候真的會覺得這種大而化簡的設計其實才是最難的,一個框架設計的足夠簡潔好用遠比功能強大來得難的多,從 React 到 Hooks 就能看到 React 團隊不是要出多高深難懂的框架或是技術,而是把整個功能封裝的好用好懂,讓更多人能上手,快速的去建構一款產品,畢竟這才是 React 誕生的目的吧!其實框架本身沒有好壞,如果一個框架適合你的產品,能為你提效,降低開發成本就是好框架。

React、jQuery 在 Goolge 歷年來搜索量

--

--