React 元件設計模式指南 Hooks, HOC 與 Render Props 實踐

me
林彥成
2023-01-25 | 5 min.
文章目錄
  1. 1. 什麼是 React 元件設計模式?
  2. 2. React 元件開發指南:從入門到最佳實踐
    1. 2.1. 元件樣式
    2. 2.2. CSS in JS
    3. 2.3. Storybook.js
  3. 3. React Component Pattern:常見設計模式解析
    1. 3.1. HOC Pattern
    2. 3.2. Render Props Pattern
    3. 3.3. Hooks Pattern
  4. 4. FAQ:React 元件開發常見問題
    1. 4.1. Q1:我該選擇 HOC 還是 Hooks?
    2. 4.2. Q2:如何優化大型 React 元件的效能?
    3. 4.3. Q3:Render Props 還有用嗎?

什麼是 React 元件設計模式?

React 元件設計模式 (Component Patterns) 是指在 React 開發中,為了提升程式碼重用性、可維護性與邏輯分離而常用的一套架構方案。常見的模式包括 Hook Pattern(利用自定義 Hook 封裝邏輯)、HOC (Higher-Order Components)(包裝元件以增強功能)以及 Render Props(透過 Function Prop 傳遞渲染邏輯)。掌握這些模式能有效解決「大型元件難以維護」的痛點,是構建複雜應用程式的核心。


以 React 來說,要寫出一個元件有 Class-based 或是 Functional 兩種方式,兩者之中 Functional 寫法較為簡單且透過 Hook Pattern 能輕易將資料邏輯與顯示分離,長期來看透過 Functional 的方式會更容易去維護 and 重構。本篇 React 元件開發指南 將重點介紹 Functional Component 的開發模式。

參考文章: 從特性淺談 React Class-based 和 Functional Component 兩種寫法之異同

在軟體或是 React 元件開發 上,我們需要考量:

  • 可維護性:程式碼應易於修改和擴展。
  • 可讀可理解性:程式碼應清晰易懂,便於團隊成員協作。
  • 可靠性:元件應在各種情況下穩定運行,並能妥善處理錯誤。

對於初學者來說,小編是推薦直接學習 Functional Component 並且搭配 Hook PatternHOC PatternRender Props Pattern 進行學習,這將有助於您快速掌握 React Pattern 的精髓。

React 元件開發指南:從入門到最佳實踐

透過 React.js 這個 component-based 的函式庫進行前端開發,我們會更專注在:

  • 開發可重用的元件
  • 使用別人開發好的元件

首先先推薦這個 Github Repo 整理厲害的函式庫們,可以點進去觀賞一下。

接下來直接看程式碼,範例為 Input 基本元件,從行數上看 Functional 的方式行數縮減不少。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Class-based
class Input extends React.Component {
constructor() {
super();
this.state = { input: "" };

this.handleInput = this.handleInput.bind(this);
}

handleInput(e) {
this.setState({ input: e.target.value });
}

render() {
<input onChange={handleInput} value={this.state.input} />;
}
}

// Functional
function Input() {
const [input, setInput] = React.useState("");

return <input onChange={(e) => setInput(e.target.value)} value={input} />;
}

比起自己開發,我們更常去使用別人開發好的元件,底下是一些使用現有元件注意事項:

  • 無障礙支援程度 (A11y)
  • 樣式是否方便客製化來配合目前網站視覺
  • 文件是否詳細
  • 瀏覽器支援
  • 看一下目前的 issue
  • 更新的速度
  • Release Note 是否詳細

元件樣式

靜態樣式的處理,傳統的 css 或是 scss 的寫法,比較適合整體 Layout 相關

  • 若在新增元件的同時寫樣式檔,在元件重構或移動時會需要較多的確認與步驟
  • 樣式檔本身是 global 的,會需要一個團隊都能遵守的命名規則

當我們需要用程式去控制動態樣式,我們可以透過以下的方式處理

  • Inline style
  • classnames 去操作動態樣式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SubmitButton({ isHidden }) {
const btnSummitClass = classNames({
btn: true,
"btn-default": true,
"cursor--not-allowed": isHidden,
});
const styleButton = { display: isHidden ? "none" : "block" };

return (
<button type="button" className={btnSummitClass} style={styleButton}>
<SkillChart nowProfile={nowProfile} />
</button>
);
}

CSS in JS

CSS in JS 解決了 CSS 命名的問題。

由於 component-based 的概念興起元件開發成為顯學,CSS in JS 提供了將樣式寫在元件中解決方案,也讓 CSS 需要從寫程式語言的角度去進行架構設計。

CSS in JS 不僅降低了維護難度也加速了開發速度,減少檔案切換和減少 class 命名錯誤等等問題,小編之前介紹過一篇 CSS in JS 的文章,歡迎大家去看看。

常見的 library 像是 css module、vanilla-extract、styled-components、styled-jsx (Next.js) 都非常好上手,更完整的將元件模組化並增加可重用性。

Storybook.js

利用套件將元件獨立進行開發與測試,這樣的套件提供了一個 Living Document 而且會馬上同步元件原始碼的更動。

並且透過可以 Demo 的 Web GUI 可以讓剛開始接觸專案的開發人員更快速的了解系統中元件的使用方法,以下為常看見的兩套工具。

小編這裡推薦 Storybook.js,透過故事書與內建的 plugin 幾乎是可以只寫好 config 就能夠直接使用。

使用故事書獨立開發可能需要注意的事項

  • Promise 取代 http request
  • 使用 setTimeout 模擬一些需要時間的動作
  • 列出所有可能的狀態
  • 利用 knobs 方便進行測試
  • 專案架構上可以盡量把故事跟元件放近一點,減少找來找去

React Component Pattern:常見設計模式解析

什麼是 React Pattern?Pattern 就是改善如何去架構程式的方法,React 常見的 Component Pattern 主要是讓程式碼能夠在元件間共用。

常見的三種 Pattern 為

  • HOC Pattern
  • Render Props Pattern
  • Hooks Pattern

HOC Pattern

類似 higher-order function (HOF) 接受一個或多個 function 作為輸入最後輸出一個 function 的概念,higher-order component (HOC) 會接受 component 作為輸入後輸出一個新的 component。

1
const EnhancedComponent = withHigherOrderComponent(WrappedComponent);

通常會取名為 with 開頭,缺點是如果同一個元件會需要多個功能就會一層包一層,小編以前就遇過包了四五層的,命名規則還有功能切割不夠好的話其實有點難 debug。

1
2
3
4
5
6
7
<withGA>
<withLayout>
<withStateTrace>
<Component />
</withStateTrace>
</withLayout>
</withAuth>

Render Props Pattern

render prop 指的是用函式的 prop 來在 React component 之間共享程式碼的技巧,相對單純簡單只要將共用的程式碼變成 render function 抽出來。

1
2
3
const renderTitle = (data) => <h1>{data.name}</h1>;

<RenderTitle render={renderTitle} />;

不論是 Class-based 與 Functional Component 都能夠使用 HOC Pattern 和 Render Props Pattern,Functional 可用性較 Class-based Component 高。

可以參考下面這張圖片,Class-based Component 較容易寫出高耦合且較難進行拆分,即使透過 HOC 也會造成多層嵌套。


圖片來源: https://www.patterns.dev/posts/hooks-pattern/

Hooks Pattern

只有 Functional 的元件能透過 Hook Pattern 來降低元件複雜度。

Hook 是鉤子的意思,我們可以想像透過鉤子把新功能掛上元件,就像是加上裝備一樣,透過不同的 hook 來增加元件的功能。

常用 useState 這個 hook 操作狀態,底下範例會操作一個 List 資料。

1
const [list, setList] = useState([]);

接著如果需要透過 API 動態更新這個狀態,這時候會需要動用到 useEffect 這個 Hook

1
2
3
4
5
const [list, setList] = useState([]);

useEffect(() => {
fetch("/members?type=standard").then((res) => res.json()).((res) => setList(res));
}, [])

接著再把相關的資料透過 JSX 渲染出來,就會完成一個基本的元件。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Members() {
const [list, setList] = useState([]);

useEffect(() => {
fetch("/members?type=standard").then((res) => res.json()).((res) => setList(res));
}, [])

return (
<ul>
{list.map((elm) => <li key={elm.id}>{elm.name}</li>)}
<ul>
);
}

透過 hook pattern 在撰寫時相當直觀簡單,不過當需要的資料來源越來越多時,會發現同一個元件裡面的情況會變成下面這樣,會需要很多個 useState 搭配 useEffect。

1
2
3
4
5
6
7
8
9
10
const [list1, setList1] = useState([]);
const [list2, setList2] = useState([]);
const [list3, setList3] = useState([]);


useEffect(() => {
fetch("/members?type=standard").then((res) => res.json()).((res) => setList1(res));
fetch("/buy-list?type=standard").then((res) => res.json()).((res) => setList2(res));
fetch("/recommend-list?type=standard").then((res) => res.json()).((res) => setList2(res));
}, [])

這時候比較好的方式是把與資料相關的邏輯改成新的 hook,將資料邏輯和渲染部分拆開就能夠簡化我們的程式碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 資料邏輯 useList1.js
function useList1() {
const [list, setList] = useState([]);

useEffect(() => {
fetch("/members?type=standard").then((res) => res.json()).((res) => setList(res));
}, [])

return { list1: list };
}


// 單純渲染
function Members() {
const { list1 } = useList1();
const { list2 } = useList2();
const { list3 } = useList3();

return (
<ul>
{list1.map((elm) => <li key={elm.id}>{elm.name}</li>)}
{list2.map((elm) => <li key={elm.id}>{elm.name}</li>)}
{list3.map((elm) => <li key={elm.id}>{elm.name}</li>)}
<ul>
);
}

當然更進階的做法會是連重構都不需要,以上的過程可以直接使用像是 swr 這套函式庫所提供的 hook 就幫我們做掉以上的流程。

https://swr.vercel.app/

寫法就會再優化成底下的官方範例:

1
2
3
4
5
6
7
8
9
import useSWR from "swr";

function Profile() {
const { data, error, isLoading } = useSWR("/api/user", fetcher);

if (error) return <div>failed to load</div>;
if (isLoading) return <div>loading...</div>;
return <div>hello {data.name}!</div>;
}

最後小編想推薦 useHooks 這個介紹 Hook 的網站,裡面有非常多很棒的範例值得參考與學習。

傳送門在這裡:

https://usehooks.com/

FAQ:React 元件開發常見問題

Q1:我該選擇 HOC 還是 Hooks?

A:Hooks 是目前 React 推薦的首選。Hooks 能更優雅地處理邏輯共享,避免了 HOC 容易產生的「元件嵌套地獄(Wrapper Hell)」。只有在需要針對 Class Component 進行功能增強,或者某些特定庫要求時,才考慮使用 HOC。

Q2:如何優化大型 React 元件的效能?

A:

  1. 拆分元件:將大型元件拆分為細碎的子元件。
  2. 使用 React.memo:避免不必要的元件重新渲染。
  3. 優化 Hooks:使用 useMemouseCallback 穩定 Props 的引用。

Q3:Render Props 還有用嗎?

A:有的。雖然 Hooks 取代了大部分 Render Props 的場景,但在開發一些高度靈活的 UI 庫(如處理複雜 Headless UI)時,Render Props 依然是傳遞渲染控制權的一種強大方式。


喜歡這篇文章,請幫忙拍拍手喔 🤣