3.1 什麼是元件?
元件是 React 的核心概念,將 UI 拆成獨立、可複用的積木。
App
/ | \
Header Main Footer
/ \
Sidebar Content
/ \
PostList PostDetail
元件的特性
| 特性 | 說明 |
|---|---|
| 獨立性 | 每個元件有自己的邏輯和渲染 |
| 可複用 | 同一個元件可以在不同地方使用 |
| 可組合 | 元件可以嵌套組合成更大的元件 |
| 單一職責 | 每個元件負責一個特定的 UI 區塊 |
3.2 函式元件 (Function Components)
React 現在以函式元件為主流(Class 元件已逐漸淘汰)。
最基本的元件
// 方法 1:函式宣告
function Welcome() {
return <h1>歡迎來到 React!</h1>;
}
// 方法 2:箭頭函式(更常用)
const Welcome = () => {
return <h1>歡迎來到 React!</h1>;
};
// 方法 3:箭頭函式 + 隱式回傳(元件內容簡單時)
const Welcome = () => <h1>歡迎來到 React!</h1>;
使用元件
function App() {
return (
<div>
<Welcome />
<Welcome />
<Welcome />
</div>
);
}
元件名稱必須以大寫字母開頭!小寫開頭會被視為 HTML 標籤。
3.3 元件組合 (Composition)
範例:建構一個完整的頁面佈局
// ============================================================
// Header 元件
// ============================================================
const Header = () => {
const navStyle = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px 32px',
backgroundColor: '#1a1a2e',
color: 'white',
};
return (
<header style={navStyle}>
<h1 style={{ margin: 0, fontSize: '1.5rem' }}>MyApp</h1>
<nav>
<a href="#" style={{ color: '#a8dadc', margin: '0 12px', textDecoration: 'none' }}>首頁</a>
<a href="#" style={{ color: '#a8dadc', margin: '0 12px', textDecoration: 'none' }}>關於</a>
<a href="#" style={{ color: '#a8dadc', margin: '0 12px', textDecoration: 'none' }}>聯絡</a>
</nav>
</header>
);
};
// ============================================================
// Sidebar 元件
// ============================================================
const Sidebar = () => {
const categories = ['科技', '生活', '旅遊', '美食', '運動'];
return (
<aside style={{
width: '200px',
padding: '20px',
backgroundColor: '#f8f9fa',
borderRight: '1px solid #dee2e6',
}}>
<h3>分類</h3>
<ul style={{ listStyle: 'none', padding: 0 }}>
{categories.map((cat, i) => (
<li key={i} style={{
padding: '8px 12px',
cursor: 'pointer',
borderRadius: '4px',
marginBottom: '4px',
}}>
{cat}
</li>
))}
</ul>
</aside>
);
};
// ============================================================
// ArticleCard 元件
// ============================================================
const ArticleCard = () => {
return (
<div style={{
border: '1px solid #dee2e6',
borderRadius: '8px',
padding: '20px',
marginBottom: '16px',
}}>
<h2 style={{ marginTop: 0 }}>文章標題</h2>
<p style={{ color: '#666' }}>
這是文章的摘要內容,簡單描述文章的重點...
</p>
<span style={{ color: '#999', fontSize: '0.85rem' }}>
2024-01-15
</span>
</div>
);
};
// ============================================================
// MainContent 元件
// ============================================================
const MainContent = () => {
return (
<main style={{ flex: 1, padding: '20px' }}>
<h2>最新文章</h2>
<ArticleCard />
<ArticleCard />
<ArticleCard />
</main>
);
};
// ============================================================
// Footer 元件
// ============================================================
const Footer = () => {
return (
<footer style={{
textAlign: 'center',
padding: '20px',
backgroundColor: '#1a1a2e',
color: '#a8dadc',
}}>
<p>© 2024 MyApp. All rights reserved.</p>
</footer>
);
};
// ============================================================
// App:組合所有元件
// ============================================================
function App() {
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<Header />
<div style={{ display: 'flex', flex: 1 }}>
<Sidebar />
<MainContent />
</div>
<Footer />
</div>
);
}
export default App;
3.4 元件檔案結構
方式一:依功能分類(推薦大型專案)
src/
├── components/
│ ├── common/ # 共用元件
│ │ ├── Button.jsx
│ │ ├── Input.jsx
│ │ └── Modal.jsx
│ ├── layout/ # 佈局元件
│ │ ├── Header.jsx
│ │ ├── Footer.jsx
│ │ └── Sidebar.jsx
│ └── features/ # 功能元件
│ ├── auth/
│ │ ├── LoginForm.jsx
│ │ └── RegisterForm.jsx
│ └── posts/
│ ├── PostList.jsx
│ └── PostDetail.jsx
├── pages/ # 頁面元件
│ ├── HomePage.jsx
│ └── AboutPage.jsx
├── App.jsx
└── main.jsx
方式二:依元件分類(推薦中小型專案)
src/
├── components/
│ ├── Button/
│ │ ├── Button.jsx
│ │ ├── Button.css
│ │ └── index.js # 方便匯入
│ ├── Header/
│ │ ├── Header.jsx
│ │ ├── Header.css
│ │ └── index.js
│ └── PostCard/
│ ├── PostCard.jsx
│ ├── PostCard.css
│ └── index.js
├── App.jsx
└── main.jsx
index.js 的用法
// components/Button/Button.jsx
const Button = ({ label, onClick }) => {
return <button onClick={onClick}>{label}</button>;
};
export default Button;
// components/Button/index.js
export { default } from './Button';
// 使用時可以簡潔地匯入
import Button from './components/Button'; // 自動找 index.js
3.5 元件匯出與匯入
Default Export(預設匯出)— 每個檔案只能有一個
// Header.jsx
const Header = () => <header>Header</header>;
export default Header;
// 匯入時可以自由命名
import Header from './Header';
import MyHeader from './Header'; // 也可以
Named Export(具名匯出)— 可以有多個
// utils.jsx
export const Button = () => <button>Button</button>;
export const Input = () => <input />;
export const Label = () => <label>Label</label>;
// 匯入時必須使用相同的名稱,用大括號
import { Button, Input } from './utils';
// 可以用 as 重新命名
import { Button as PrimaryButton } from './utils';
混合使用
// components.jsx
const MainComponent = () => <div>Main</div>;
export const Helper1 = () => <span>Helper 1</span>;
export const Helper2 = () => <span>Helper 2</span>;
export default MainComponent;
// 匯入
import MainComponent, { Helper1, Helper2 } from './components';
3.6 純元件的概念
React 元件應該像純函式一樣運作:相同的輸入(props)應該產生相同的輸出(JSX)。
// ✅ 純元件:相同的 props 永遠回傳相同的 JSX
const Greeting = ({ name }) => {
return <h1>Hello, {name}!</h1>;
};
// ❌ 不純的元件:包含副作用(每次渲染結果不同)
const BadGreeting = ({ name }) => {
// 修改了外部變數 → 副作用
document.title = name;
return <h1>Hello, {name}!</h1>;
};
// ❌ 不純的元件:依賴外部可變狀態
let count = 0;
const BadCounter = () => {
count++; // 每次渲染都會改變
return <p>渲染次數:{count}</p>;
};
副作用(如修改 DOM、API 呼叫)應該放在
useEffect中處理(後面的課程會教)。
3.7 完整範例:電商商品展示頁
// ============================================================
// 商品資料
// ============================================================
const products = [
{
id: 1,
name: 'MacBook Pro 16"',
price: 79900,
originalPrice: 89900,
image: '💻',
category: '電腦',
rating: 4.8,
reviews: 256,
inStock: true,
},
{
id: 2,
name: 'AirPods Pro',
price: 7490,
originalPrice: 7490,
image: '🎧',
category: '配件',
rating: 4.6,
reviews: 1024,
inStock: true,
},
{
id: 3,
name: 'iPad Air',
price: 19900,
originalPrice: 22900,
image: '📱',
category: '平板',
rating: 4.7,
reviews: 512,
inStock: false,
},
];
// ============================================================
// 星星評分元件
// ============================================================
const StarRating = ({ rating, reviews }) => {
const fullStars = Math.floor(rating);
const hasHalfStar = rating % 1 >= 0.5;
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ color: '#f59e0b' }}>
{'★'.repeat(fullStars)}
{hasHalfStar && '☆'}
{'☆'.repeat(5 - fullStars - (hasHalfStar ? 1 : 0))}
</span>
<span style={{ color: '#999', fontSize: '0.85rem' }}>
({reviews})
</span>
</div>
);
};
// ============================================================
// 價格元件
// ============================================================
const PriceDisplay = ({ price, originalPrice }) => {
const hasDiscount = price < originalPrice;
const discountPercent = hasDiscount
? Math.round((1 - price / originalPrice) * 100)
: 0;
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '1.3rem', fontWeight: 'bold', color: '#e53e3e' }}>
NT$ {price.toLocaleString()}
</span>
{hasDiscount && (
<>
<span style={{
textDecoration: 'line-through',
color: '#999',
fontSize: '0.9rem',
}}>
NT$ {originalPrice.toLocaleString()}
</span>
<span style={{
backgroundColor: '#fed7d7',
color: '#e53e3e',
padding: '2px 6px',
borderRadius: '4px',
fontSize: '0.75rem',
fontWeight: 'bold',
}}>
-{discountPercent}%
</span>
</>
)}
</div>
);
};
// ============================================================
// 庫存標籤元件
// ============================================================
const StockBadge = ({ inStock }) => {
return (
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '0.8rem',
fontWeight: 'bold',
backgroundColor: inStock ? '#c6f6d5' : '#fed7d7',
color: inStock ? '#22543d' : '#9b2c2c',
}}>
{inStock ? '有庫存' : '暫時缺貨'}
</span>
);
};
// ============================================================
// 商品卡片元件
// ============================================================
const ProductCard = ({ product }) => {
return (
<div style={{
border: '1px solid #e2e8f0',
borderRadius: '12px',
padding: '20px',
backgroundColor: 'white',
transition: 'box-shadow 0.2s',
cursor: 'pointer',
}}>
<div style={{ fontSize: '4rem', textAlign: 'center', marginBottom: '12px' }}>
{product.image}
</div>
<span style={{ color: '#718096', fontSize: '0.8rem' }}>
{product.category}
</span>
<h3 style={{ margin: '8px 0', fontSize: '1.1rem' }}>
{product.name}
</h3>
<StarRating rating={product.rating} reviews={product.reviews} />
<div style={{ marginTop: '12px' }}>
<PriceDisplay price={product.price} originalPrice={product.originalPrice} />
</div>
<div style={{ marginTop: '12px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<StockBadge inStock={product.inStock} />
<button
disabled={!product.inStock}
style={{
padding: '8px 16px',
borderRadius: '6px',
border: 'none',
backgroundColor: product.inStock ? '#4299e1' : '#cbd5e0',
color: 'white',
cursor: product.inStock ? 'pointer' : 'not-allowed',
fontWeight: 'bold',
}}
>
{product.inStock ? '加入購物車' : '缺貨中'}
</button>
</div>
</div>
);
};
// ============================================================
// 商品列表元件
// ============================================================
const ProductList = () => {
return (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: '20px',
padding: '20px',
}}>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
};
// ============================================================
// App
// ============================================================
function App() {
return (
<div style={{ maxWidth: '1200px', margin: '0 auto', fontFamily: 'sans-serif' }}>
<h1 style={{ textAlign: 'center', padding: '20px' }}>
商品展示
</h1>
<ProductList />
</div>
);
}
export default App;
注意這個範例如何將 UI 拆分成小而專注的元件:
StarRating、PriceDisplay、StockBadge、ProductCard、ProductList, 每個元件只負責一件事。
3.8 練習題
練習 1:建立一個個人履歷頁面
使用至少 5 個元件來組合一個履歷頁面:
- ProfileHeader:大頭照、姓名、職稱
- SkillList:技能列表
- ExperienceCard:工作經歷
- EducationCard:學歷
- ContactInfo:聯絡資訊
練習 2:建立一個餐廳菜單
MenuCategory:類別名稱(前菜、主餐、甜點)MenuItem:菜名、價格、描述MenuPage:組合所有類別
本課重點回顧
- 元件是 React UI 的基本單位
- 函式元件是目前的主流寫法
- 元件名稱必須以大寫字母開頭
- 透過組合小元件來建構大元件
- 善用 export / import 管理元件
- 元件應該像純函式一樣運作