20.1 高階元件 (Higher-Order Components, HOC)

HOC 是一個函式,接收一個元件並回傳一個增強後的新元件。

// ============================================================
// 建立 HOC:加入載入狀態
// ============================================================
function withLoading(WrappedComponent) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) {
      return (
        <div style={{ textAlign: 'center', padding: '40px', color: '#a0aec0' }}>
          載入中...
        </div>
      );
    }
    return <WrappedComponent {...props} />;
  };
}

// 原始元件
function UserList({ users }) {
  return (
    <ul>
      {users.map((u) => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

// 增強後的元件
const UserListWithLoading = withLoading(UserList);

// 使用
function App() {
  const [loading, setLoading] = useState(true);
  const [users, setUsers] = useState([]);

  return <UserListWithLoading isLoading={loading} users={users} />;
}

HOC:加入權限檢查

function withAuth(WrappedComponent, requiredRole) {
  return function WithAuthComponent(props) {
    const user = { role: 'admin' }; // 實際應從 context 或 store 取得

    if (!user) {
      return <p>請先登入</p>;
    }

    if (requiredRole && user.role !== requiredRole) {
      return <p>您沒有權限存取此頁面</p>;
    }

    return <WrappedComponent {...props} user={user} />;
  };
}

const AdminPanel = withAuth(({ user }) => {
  return <h1>管理員面板 — 歡迎 {user.role}</h1>;
}, 'admin');

注意:現代 React 更推薦使用自訂 Hook 來取代大多數 HOC 的使用場景。


20.2 Render Props

元件透過 prop 接收一個回傳 JSX 的函式,將內部狀態「暴露」給外部使用。

import { useState, useEffect } from 'react';

// ============================================================
// Render Props 元件:滑鼠追蹤
// ============================================================
function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return render(position);
}

// 使用方式 1:顯示座標
function App() {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <p>滑鼠位置:({x}, {y})</p>
      )}
    />
  );
}

// 使用方式 2:跟隨滑鼠的圓點
function App2() {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <div style={{
          position: 'fixed',
          left: x - 10,
          top: y - 10,
          width: 20,
          height: 20,
          borderRadius: '50%',
          backgroundColor: '#4299e1',
          pointerEvents: 'none',
        }} />
      )}
    />
  );
}

現在大多可以用自訂 Hook 取代 Render Props。


20.3 Compound Components 複合元件

<select> + <option> 一樣,多個元件隱式共享狀態

import { createContext, useContext, useState } from 'react';

// ============================================================
// 手風琴 (Accordion) 複合元件
// ============================================================

const AccordionContext = createContext();

function Accordion({ children, allowMultiple = false }) {
  const [openItems, setOpenItems] = useState(new Set());

  const toggleItem = (id) => {
    setOpenItems((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        if (!allowMultiple) next.clear();
        next.add(id);
      }
      return next;
    });
  };

  return (
    <AccordionContext.Provider value={{ openItems, toggleItem }}>
      <div style={{ border: '1px solid #e2e8f0', borderRadius: '8px', overflow: 'hidden' }}>
        {children}
      </div>
    </AccordionContext.Provider>
  );
}

function AccordionItem({ id, children }) {
  return <div style={{ borderBottom: '1px solid #e2e8f0' }}>{children}</div>;
}

function AccordionHeader({ id, children }) {
  const { openItems, toggleItem } = useContext(AccordionContext);
  const isOpen = openItems.has(id);

  return (
    <button
      onClick={() => toggleItem(id)}
      style={{
        width: '100%',
        padding: '16px',
        border: 'none',
        backgroundColor: isOpen ? '#f7fafc' : 'white',
        cursor: 'pointer',
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        fontSize: '1rem',
        fontWeight: 'bold',
        textAlign: 'left',
      }}
    >
      {children}
      <span style={{
        transform: isOpen ? 'rotate(180deg)' : 'rotate(0)',
        transition: 'transform 0.2s',
      }}>
        ▼
      </span>
    </button>
  );
}

function AccordionPanel({ id, children }) {
  const { openItems } = useContext(AccordionContext);
  const isOpen = openItems.has(id);

  if (!isOpen) return null;

  return (
    <div style={{ padding: '16px', backgroundColor: '#f7fafc' }}>
      {children}
    </div>
  );
}

// 將子元件掛載到主元件上
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;

// ============================================================
// 使用
// ============================================================
function FAQ() {
  return (
    <div style={{ maxWidth: '600px', margin: '20px auto' }}>
      <h2>常見問題</h2>
      <Accordion>
        <Accordion.Item>
          <Accordion.Header id="q1">什麼是 React?</Accordion.Header>
          <Accordion.Panel id="q1">
            <p>React 是 Meta 開發的 JavaScript UI 函式庫,用於建構使用者介面。</p>
          </Accordion.Panel>
        </Accordion.Item>

        <Accordion.Item>
          <Accordion.Header id="q2">什麼是 JSX?</Accordion.Header>
          <Accordion.Panel id="q2">
            <p>JSX 是 JavaScript 的語法擴展,讓你在 JS 中撰寫類似 HTML 的程式碼。</p>
          </Accordion.Panel>
        </Accordion.Item>

        <Accordion.Item>
          <Accordion.Header id="q3">什麼是 Hooks?</Accordion.Header>
          <Accordion.Panel id="q3">
            <p>Hooks 讓你在函式元件中使用 state 和其他 React 功能。</p>
          </Accordion.Panel>
        </Accordion.Item>
      </Accordion>
    </div>
  );
}

20.4 React.lazy 與 Suspense — 程式碼分割

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// 動態載入:只在需要時才載入該元件的 JavaScript
const HomePage = lazy(() => import('./pages/HomePage'));
const AboutPage = lazy(() => import('./pages/AboutPage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));

function LoadingSpinner() {
  return (
    <div style={{
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      minHeight: '200px',
      color: '#a0aec0',
    }}>
      載入頁面中...
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<LoadingSpinner />}>
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/about" element={<AboutPage />} />
          <Route path="/dashboard" element={<DashboardPage />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

20.5 設計模式比較

模式 適用場景 現代替代方案
HOC 橫切關注點(認證、日誌) 自訂 Hook
Render Props 共享狀態邏輯 自訂 Hook
Compound Components UI 元件庫(Tab, Accordion) 仍然是最佳選擇
自訂 Hook 幾乎所有邏輯複用
React.lazy 程式碼分割

20.6 練習題

練習 1:建立 Tabs 複合元件

// 目標 API:
<Tabs defaultValue="tab1">
  <Tabs.List>
    <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
    <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="tab1">內容 1</Tabs.Content>
  <Tabs.Content value="tab2">內容 2</Tabs.Content>
</Tabs>

練習 2:建立一個 withLogger HOC

在元件掛載、更新、卸載時自動在 console 輸出日誌。


本課重點回顧

  1. HOC:接收元件、回傳增強元件(現在多用自訂 Hook)
  2. Render Props:透過函式 prop 分享狀態(現在多用自訂 Hook)
  3. Compound Components:多個元件隱式共享狀態(仍然常用)
  4. React.lazy + Suspense:程式碼分割,按需載入
  5. 現代 React 的首選是自訂 Hook