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 輸出日誌。
本課重點回顧
- HOC:接收元件、回傳增強元件(現在多用自訂 Hook)
- Render Props:透過函式 prop 分享狀態(現在多用自訂 Hook)
- Compound Components:多個元件隱式共享狀態(仍然常用)
- React.lazy + Suspense:程式碼分割,按需載入
- 現代 React 的首選是自訂 Hook