React Compound Component

January 26, 2025

  • react
有任何問題,我能幫上忙的話都能到 github 發 issue 問我。

Preface

Compound Component (復合元件) 是一個程式設計概念,他也出現在滿多的 lib 中,在 react 中尤其是 headleass UI component,他算是舉足輕重的角色,去年因為公司主管的要求,我將公司的部分網站,轉往使用 react,經過一番 search 後,我們的元件庫是使用 shacn ui,這套也是使用 compound component,所以今天我就來解釋一下這是什麼樣的設計理念。

Goal

我覺得要使用某種方式去 coding,你最先需要知道目的是為何?我相信各位使用 react 應該有看過類似這種的程式碼。

function Controller() {
  return (
    <Controller
      search={search}
      setSearch={setSearch}
      text={text}
      setText={setText}
      history={history}
      setHistory={setHistory}
      handleStep={handleStep}
      scroll={scroll}
      fontSize={fontSize}
      setFontSize={setFontSize}
      ...
    />
  )
}

其實這程式碼沒有不對,但假設這個 Controller 他在其他頁面需要重復使用,且在那個頁面不需要有修改 fontSize 的元件,一般的情況你會在拋一個 props 進去,像是 disableFontSize 他是個 boolean,或著你覺得需要做到每個功能都可控,所以也可以會去規劃有一個 disabledFeatures 然後他是 array,["search", "history", "text", "fontSize"],類似這種方式去控制元件。

以上都是很好的解決方案,但還有一種設計思路是這樣,當你不要某個功能其實就將他移除掉,這樣程式碼會更精簡,這就是我今天想打得 Compound Component,的設計理念。

function Controller() {
  return (
    <Controller>
      <Controller.Container>
        {/* <Controller.Text /> */}
        <Controller.Search />
        <Controller.History />
        <Controller.FontSize />
      </Controller.Container>
    </Controller>
  )
}

在 react 中,為了實現上述功能,會使用 context,來實作,這樣即使你的 component 元件有很多層嵌套的比較深,也不會有 props drilling 的問題出現,也方便元件 state 的集中管理,也不用每個元件都拋個 props 給他。

遵循這種設計思路,所以通常父層就是在定義 context,接著 Container 大部分則是定義元件的輪廓,這也取決於你想怎麼擺放,剩下的則是將不同的元件樣式及功能做抽離,這樣講有點籠統,我就實際實作看看。

Hand Dirty

可以想像一個電商的商品圖,他會有圖片,價錢,商品名稱,評價…等,我身為一個工程師得去怎樣規劃呢?

import React, { createContext, useContext, useState } from "react"

interface ProductContextType {
  picture?: string
  name: string
  price: number
  rank?: number
  comments?: string[]
}

const ProductContext = createContext<ProductContextType | null>(null)

function Product({
  children,
  picture,
  name,
  rank,
  price,
  comments,
}: {
  children: React.ReactNode
  picture?: string
  name: string
  price: string
  rank?: number
  comments?: string[]
}) {
  // state 的部分請看自身需求,如果只需要傳遞就不用定義 ex: name
  const [productPicture, _setProductPicture] = useState(
    picture ?? "https://picsum.photos/id/237/200/300"
  )
  const [productPrice, _setProductPrice] = useState(price ?? 0)
  const [productRank, _setProductRank] = useState(rank ?? 0)
  const [productComments, _setProductComments] = useState(comments ?? [])

  return (
    <ProductContext.Provider
      value={{
        picture: productPicture,
        name,
        price: productPrice,
        rank: productRank,
        comments: productComments,
      }}
    >
      {children}
    </ProductContext.Provider>
  )
}

function Container({ children }: { children: React.ReactNode }) {
  return <div className="p-2">{children}</div>
}

function Picture() {
  const { picture } = useContext(ProductContext) as ProductContextType

  return <img src={picture} alt="product img" />
}

function Price() {
  const { price } = useContext(ProductContext) as ProductContextType

  return <div className="text-blue-500 font-bold">{price}</div>
}

function Name() {
  const { name } = useContext(ProductContext) as ProductContextType

  return <div className="font-bold">{name}</div>
}

function Ranking() {
  const { ranking } = useContext(ProductContext) as ProductContextType

  return <div className="font-bold text-red-500">{ranking}</div>
}

function Comments() {
  const { comments } = useContext(ProductContext) as ProductContextType

  return (
    <ul className="font-bold">
      {comments.map((comment, index) => (
        <li key={index}>{comment}</li>
      ))}
    </ul>
  )
}

Product.Container = Container
Product.Picture = Picture
Product.Name = Name
Product.Price = Price
Product.Ranking = Ranking
Product.Comments = Comments

export default Product

按照 compund component 的設計思路,我自己個人傾向會將其定義成上面這樣,效果如下圖:

compund component

你可以依據自己的需求去引入相對應的元件,需要時再將其引入就好。

Conclusion

其實 compound component 這種設計思路,已經出現很久了,那為何我現在會想要寫這篇文章呢?原因在於,這就是我個人認為工程師現階段還不會被 AI 取代的原因之一,現階段的大語言模型基本上就是把 wireframe 甚至是功能一籮筐的打印出來,坦白說也的確滿厲害的,問一問問題就可以把簡單的 App 寫出來,但是大語言模型還是有瑕疵的,當你的元件是需要更具有彈性或可擴性時,A 要 link 到 B 時,這部分目前還是得由人類去推敲及規劃怎樣做,AI 給你的解不見得是最佳解,每家公司甚至是你自己的產品需求也都不同,由工程師去決定哪個是比較適合自己的解決方案。

不斷精進自己讓自己不會被 AI 取代是我認為目前工程師滿重要的課題。


Mayvis Chen
你好,我是 Mayvis Chen
住在台北,會點前端,會點後端,熱愛分享新知及愛喝奶茶的全端打雜工程師。