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 拆分成小而專注的元件: StarRatingPriceDisplayStockBadgeProductCardProductList, 每個元件只負責一件事。


3.8 練習題

練習 1:建立一個個人履歷頁面

使用至少 5 個元件來組合一個履歷頁面: - ProfileHeader:大頭照、姓名、職稱 - SkillList:技能列表 - ExperienceCard:工作經歷 - EducationCard:學歷 - ContactInfo:聯絡資訊

練習 2:建立一個餐廳菜單

  • MenuCategory:類別名稱(前菜、主餐、甜點)
  • MenuItem:菜名、價格、描述
  • MenuPage:組合所有類別

本課重點回顧

  1. 元件是 React UI 的基本單位
  2. 函式元件是目前的主流寫法
  3. 元件名稱必須以大寫字母開頭
  4. 透過組合小元件來建構大元件
  5. 善用 export / import 管理元件
  6. 元件應該像純函式一樣運作