今天要介紹的是 React 的 Hook 模式,嚴格來說 Hooks 不一定是一種設計模式,但它可以替代很多傳統的設計模式。
介紹 Hooks 前,先來看看 React class component 以及它的一些缺點。
在一個 class component 中,會包含該元件的狀態、生命週期方法和客製化方法,示意程式碼如下:
class MyComponent extends React.Component {
/* 定義元件狀態,綁定客製化方法 */
constructor() {
super()
this.state = { ... }
this.customMethodOne = this.customMethodOne.bind(this)
this.customMethodTwo = this.customMethodTwo.bind(this)
}
/* 生命週期方法 */
componentDidMount() { ...}
componentWillUnmount() { ... }
/* 客製化方法 */
customMethodOne() { ... }
customMethodTwo() { ... }
/* 要渲染到畫面的 element */
render() { return { ... }}
}
class component 有下列幾項缺點:
如果我們原有一個單純的按鈕元件如下:
function Button() {
return <div className="btn">disabled</div>;
}
一切運作都沒問題,但如果需要為按鈕加上狀態切換的邏輯,當使用者點擊按鈕後切換 enabled 的狀態,那就需要改寫為 class component 來定義和處理元件狀態,改寫後如下:
export default class Button extends React.Component {
constructor() {
super();
this.state = { enabled: false };
}
render() {
const { enabled } = this.state;
const btnText = enabled ? "enabled" : "disabled";
return (
<div
className={`btn enabled-${enabled}`}
onClick={() => this.setState({ enabled: !enabled })}
>
{btnText}
</div>
);
}
}
在改寫過程中,開發者需要熟悉 JavaScript class 和 this 運作原理,以免出錯,因此要耗費較多心力來重構,對開發者心智負擔較大。
另外,如果想要在多個 class component 間共用邏輯,可使用 HOC 或 Render Prop 模式,但若要使用這些模式,可能又要重構 component 程式碼,無法輕易加上共用邏輯,且當共享邏輯變多時,可能會形成多層的包裝元件,導致「包裝地獄」如下,包裝地獄會讓資料流變得難以理解,也相對更難除錯,降低程式碼的可讀性與可維護性。
// 包裝地獄
<WrapperOne>
<WrapperTwo>
<WrapperThree>
<WrapperFour>
<WrapperFive>
<Component>
<h1>Finally in the component!</h1>
</Component>
</WrapperFive>
</WrapperFour>
</WrapperThree>
</WrapperTwo>
</WrapperOne>
當邏輯變多時,class component 的大小會逐漸增加,程式碼逐漸變得冗長,且邏輯會散落在各生命週期方法內,以一個要處理訂閱文章的副作用邏輯來說,class component 需要透過 3 個生命週期方法才能處理好副作用:
(關於副作用處理的更多說明,詳情請見[React] React 中的副作用處理、初探 useEffect)
componentDidMount(){
//mount 後進行副作用處理
articleAPI.subscribeUpdates(
this.props.id,
this.handleArticleUpdate
)
}
componentDidUpdate(){
//取消訂閱前一次 render 版本的 props.id 的文章
articleAPI.unsubscribeUpdates(
prevProps.id,
this.handleArticleUpdate
);
//訂閱這次 render 的 props.id 的文章
articleAPI.subscribeUpdates(
this.props.id,
this.handleArticleUpdate
);
}
componentWillUnmount(){
//清除副作用的影響
articleAPI.unsubscribeUpdates(
this.props.id,
this.handleArticleUpdate
)
}
可看出 class component 需要透過生命週期方法來處理副作用,開發者要在 componentDidMount
、componentDidUpdate
和 componentWillUnmount
中分別考慮如何將資料同步到外部系統,並判斷各生命週期中應該做什麼來達到同步效果。當應用變複雜時就容易出錯。另外,component 內可能有多個副作用處理,不同副作用處理同時寫在 componentDidMount
、componentDidUpdate
和 componentWillUnmount
中,容易產生衝突、難以維護與除錯。
介紹完 class component 與其問題後,來看看 Hooks 吧! React Hooks 的出現就是為了解決 class component 的問題,React hooks 可以:
componentDidMount
和 componentWillUnMount
接下來簡要介紹開發上常用的 useState
和 useEffect
hooks,因為之前在 Medium 文章已經詳細介紹過這兩個 hooks,詳情請見:[React] 認識狀態管理機制 state 與畫面更新機制 reconciliation、[React] React 中的副作用處理、初探 useEffect,這裡就先簡單介紹,如果有想深入了解的可以再看看我之前的文章!
useState
hookuseState
hook 可管理 function component 內的狀態,useState
使用方式如下。
import { useState } from 'react';
export default function App(props){
// 將 useState 回傳的值做陣列解構,第一個元素是「該次 render 的當前 state 值」,第二個元素是「用來更新 state 值的 setState 方法」,是一個函式
// useState()傳入的參數 initialState 是 state 的初始值,可以是任意型別的值
const [state, setState] = useState(initialState);
//...
}
如果我們希望用 state 來控制使用者在 input 欄位輸入的值,可將 input 和 useState
搭配使用如下。
import { useState } from 'react';
function Input() {
const [inputVal, setInputVal] = useState(""); // 使用輸入值,初始值為空字串
// 當 input 的值改變時,呼叫 setInputVal 來改變 input 的 state 值
// input 欄位的 value 就是我們儲存的 inputVal state 值
return <input onChange={(e) => setInputVal(e.target.value)} value={inputVal} />;
}
useEffect
hookuseEffect
可用來管理 component 內的副作用,並且讓開發者不須再使用生命週期方法來處理各生命週期階段要做的事,只要單純使用一個 useEffect
來處理即可。useEffect
呼叫方式:
import { useEffect } from 'react';
export default function App(props){
//...
useEffect(effectFunction, dependencies?)
//...
}
Object.is
一一比較陣列中所有依賴項目的值與前一次 render 版本的是否相同,若相同,則跳過執行此次 render 的 effect 函式如果我們希望 input 的值改變時,能用 console.log
印出目前的值,就可使用 useEffect
來讓 console.log
的行為和 input 的值同步,換句話說,我們用 useEffect
來處理 console.log
的副作用行為。程式碼如下。
import { useState, useEffect } from 'react';
export default function Input() {
const [inputVal, setInputVal] = useState("");
useEffect(() => {
console.log(`The user typed ${inputVal}`);
}, [inputVal]);
return (
<input
onChange={e => setInputVal(e.target.value)}
value={inputVal}
/>
);
}
使用 React hooks 時有幾項規則需要遵守:
hooks 有這些限制是為了確保 hooks 機制正確運作,沒遵守這規定可能導致資料丟失問題,導致出現預期外的錯誤,詳細請見[React] 認識 useCallback、useMemo,了解 hooks 運作原理文章後半有說明。
使用 Hooks 的優點如下:
使用 Hooks 的缺點如下:
useEffect
)useCallback
、useMemo
)