4.1 什麼是 Props?

Props(Properties)是父元件傳遞資料給子元件的機制,類似 HTML 標籤的屬性。

// 父元件傳遞 props
<UserCard name="Alice" age={25} isOnline={true} />

// 子元件接收 props
const UserCard = (props) => {
  return (
    <div>
      <h2>{props.name}</h2>
      <p>年齡:{props.age}</p>
      <p>狀態:{props.isOnline ? '在線' : '離線'}</p>
    </div>
  );
};

Props 的核心規則

規則 說明
單向資料流 資料只能從父元件流向子元件
唯讀 (Read-Only) 子元件不能修改收到的 props
任意類型 可以傳遞字串、數字、布林、陣列、物件、函式、JSX

4.2 Props 的解構 (Destructuring)

// 方式 1:在參數中直接解構(最常用)
const UserCard = ({ name, age, isOnline }) => {
  return (
    <div>
      <h2>{name}</h2>
      <p>年齡:{age}</p>
      <p>{isOnline ? '🟢 在線' : '🔴 離線'}</p>
    </div>
  );
};

// 方式 2:在函式內解構
const UserCard = (props) => {
  const { name, age, isOnline } = props;
  return (
    <div>
      <h2>{name}</h2>
      <p>年齡:{age}</p>
      <p>{isOnline ? '🟢 在線' : '🔴 離線'}</p>
    </div>
  );
};

// 方式 3:解構 + 剩餘參數
const UserCard = ({ name, age, ...otherProps }) => {
  console.log(otherProps); // { isOnline: true, role: 'admin' }
  return <div>{name}, {age}</div>;
};

4.3 Props 的類型

可以傳遞的資料類型

function App() {
  const handleClick = () => alert('被點擊了!');

  return (
    <DemoComponent
      // 字串
      title="Hello"

      // 數字
      count={42}

      // 布林值
      isActive={true}

      // 布林值簡寫(等同 isVisible={true})
      isVisible

      // 陣列
      items={['蘋果', '香蕉', '橘子']}

      // 物件
      user={{ name: 'Alice', age: 25 }}

      // 函式
      onClick={handleClick}

      // JSX 元素
      icon={<span>⭐</span>}

      // undefined / null(不會渲染)
      empty={null}
    />
  );
}

const DemoComponent = ({
  title,
  count,
  isActive,
  isVisible,
  items,
  user,
  onClick,
  icon,
  empty,
}) => {
  return (
    <div>
      <h1>{title}</h1>
      <p>Count: {count}</p>
      <p>Active: {isActive ? 'Yes' : 'No'}</p>
      <p>Visible: {isVisible ? 'Yes' : 'No'}</p>
      <ul>
        {items.map((item, i) => <li key={i}>{item}</li>)}
      </ul>
      <p>User: {user.name}, {user.age}</p>
      <button onClick={onClick}>點我</button>
      <div>{icon}</div>
      <div>{empty}</div>
    </div>
  );
};

4.4 預設值 (Default Props)

// 方式 1:解構時給預設值(推薦)
const Button = ({
  text = '按鈕',
  color = '#4299e1',
  size = 'medium',
  disabled = false,
}) => {
  const sizeMap = {
    small: { padding: '4px 8px', fontSize: '0.8rem' },
    medium: { padding: '8px 16px', fontSize: '1rem' },
    large: { padding: '12px 24px', fontSize: '1.2rem' },
  };

  return (
    <button
      disabled={disabled}
      style={{
        ...sizeMap[size],
        backgroundColor: disabled ? '#ccc' : color,
        color: 'white',
        border: 'none',
        borderRadius: '6px',
        cursor: disabled ? 'not-allowed' : 'pointer',
      }}
    >
      {text}
    </button>
  );
};

// 使用
function App() {
  return (
    <div style={{ display: 'flex', gap: '8px', padding: '20px' }}>
      <Button />
      <Button text="提交" color="#48bb78" />
      <Button text="刪除" color="#e53e3e" size="small" />
      <Button text="不可用" disabled />
      <Button text="大按鈕" size="large" color="#805ad5" />
    </div>
  );
}

4.5 children Props

children 是一個特殊的 prop,代表元件開始標籤和結束標籤之間的內容。

// ============================================================
// 基本 children 用法
// ============================================================
const Card = ({ children, title }) => {
  return (
    <div style={{
      border: '1px solid #e2e8f0',
      borderRadius: '8px',
      overflow: 'hidden',
      marginBottom: '16px',
    }}>
      {title && (
        <div style={{
          padding: '12px 16px',
          backgroundColor: '#f7fafc',
          borderBottom: '1px solid #e2e8f0',
          fontWeight: 'bold',
        }}>
          {title}
        </div>
      )}
      <div style={{ padding: '16px' }}>
        {children}
      </div>
    </div>
  );
};

function App() {
  return (
    <div style={{ maxWidth: '500px', margin: '20px auto' }}>
      <Card title="使用者資訊">
        <p>姓名:Alice</p>
        <p>Email:alice@example.com</p>
      </Card>

      <Card title="通知">
        <p style={{ color: '#e53e3e' }}>您有 3 則未讀訊息</p>
        <button>查看全部</button>
      </Card>

      <Card>
        <p>這張卡片沒有標題</p>
      </Card>
    </div>
  );
}

用 children 建立佈局元件

// ============================================================
// 通用容器元件
// ============================================================
const Container = ({ children, maxWidth = '1200px' }) => (
  <div style={{
    maxWidth,
    margin: '0 auto',
    padding: '0 16px',
  }}>
    {children}
  </div>
);

// ============================================================
// 兩欄佈局元件
// ============================================================
const TwoColumnLayout = ({ sidebar, children }) => (
  <div style={{ display: 'flex', gap: '24px' }}>
    <aside style={{ width: '250px', flexShrink: 0 }}>
      {sidebar}
    </aside>
    <main style={{ flex: 1 }}>
      {children}
    </main>
  </div>
);

// ============================================================
// 使用佈局元件
// ============================================================
function App() {
  return (
    <Container maxWidth="960px">
      <TwoColumnLayout
        sidebar={
          <nav>
            <h3>導航</h3>
            <ul>
              <li>首頁</li>
              <li>文章</li>
              <li>關於</li>
            </ul>
          </nav>
        }
      >
        <h1>主要內容</h1>
        <p>這是右側的主要內容區域</p>
      </TwoColumnLayout>
    </Container>
  );
}

4.6 Props 傳遞技巧

展開運算子傳遞 Props

const userProps = {
  name: 'Alice',
  age: 25,
  email: 'alice@example.com',
  role: 'admin',
};

// 逐一傳遞
<UserProfile name={userProps.name} age={userProps.age} email={userProps.email} role={userProps.role} />

// 使用展開運算子(更簡潔)
<UserProfile {...userProps} />

// 展開後覆蓋特定值
<UserProfile {...userProps} role="viewer" />

從父元件傳遞函式(子元件向上通訊)

// ============================================================
// 父元件
// ============================================================
function ParentComponent() {
  const handleChildMessage = (message) => {
    alert(`來自子元件的訊息:${message}`);
  };

  return (
    <div>
      <h1>父元件</h1>
      <ChildComponent onSendMessage={handleChildMessage} />
    </div>
  );
}

// ============================================================
// 子元件 — 透過呼叫 props 中的函式來「回傳」資料給父元件
// ============================================================
const ChildComponent = ({ onSendMessage }) => {
  return (
    <div style={{ border: '2px dashed #ccc', padding: '16px', margin: '16px' }}>
      <h2>子元件</h2>
      <button onClick={() => onSendMessage('Hello from Child!')}>
        發送訊息給父元件
      </button>
    </div>
  );
};

4.7 PropTypes 型別檢查

PropTypes 可以在開發階段檢查 props 的類型是否正確。

npm install prop-types
import PropTypes from 'prop-types';

const UserCard = ({ name, age, email, hobbies, address, onEdit }) => {
  return (
    <div>
      <h2>{name}</h2>
      <p>年齡:{age}</p>
      <p>Email:{email}</p>
      <p>興趣:{hobbies.join(', ')}</p>
      <p>地址:{address.city}, {address.country}</p>
      <button onClick={onEdit}>編輯</button>
    </div>
  );
};

UserCard.propTypes = {
  // 必填字串
  name: PropTypes.string.isRequired,

  // 必填數字
  age: PropTypes.number.isRequired,

  // 選填字串
  email: PropTypes.string,

  // 字串陣列
  hobbies: PropTypes.arrayOf(PropTypes.string),

  // 物件(指定結構)
  address: PropTypes.shape({
    city: PropTypes.string.isRequired,
    country: PropTypes.string.isRequired,
    zipCode: PropTypes.string,
  }),

  // 函式
  onEdit: PropTypes.func,

  // 限定值
  role: PropTypes.oneOf(['admin', 'editor', 'viewer']),

  // 多種類型
  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

UserCard.defaultProps = {
  email: '未提供',
  hobbies: [],
  address: { city: '未知', country: '未知' },
  onEdit: () => {},
};

注意:如果使用 TypeScript,可以用 interface / type 取代 PropTypes,更加強大。


4.8 完整範例:通知中心元件

// ============================================================
// 通知資料
// ============================================================
const notifications = [
  {
    id: 1,
    type: 'success',
    title: '訂單成功',
    message: '您的訂單 #12345 已確認',
    time: '2 分鐘前',
    read: false,
  },
  {
    id: 2,
    type: 'warning',
    title: '庫存警告',
    message: '商品 "AirPods Pro" 庫存剩餘 5 件',
    time: '15 分鐘前',
    read: false,
  },
  {
    id: 3,
    type: 'error',
    title: '付款失敗',
    message: '訂單 #12340 的付款處理失敗',
    time: '1 小時前',
    read: true,
  },
  {
    id: 4,
    type: 'info',
    title: '系統更新',
    message: '系統將於今晚 2:00 AM 進行維護',
    time: '3 小時前',
    read: true,
  },
];

// ============================================================
// 圖示元件
// ============================================================
const NotificationIcon = ({ type }) => {
  const icons = {
    success: '✅',
    warning: '⚠️',
    error: '❌',
    info: 'ℹ️',
  };
  return <span style={{ fontSize: '1.5rem' }}>{icons[type] || 'ℹ️'}</span>;
};

// ============================================================
// 單一通知元件
// ============================================================
const NotificationItem = ({
  type,
  title,
  message,
  time,
  read,
  onMarkRead,
  onDelete,
}) => {
  const colors = {
    success: '#48bb78',
    warning: '#ed8936',
    error: '#fc8181',
    info: '#63b3ed',
  };

  return (
    <div style={{
      display: 'flex',
      alignItems: 'flex-start',
      gap: '12px',
      padding: '16px',
      borderLeft: `4px solid ${colors[type]}`,
      backgroundColor: read ? '#fff' : '#f7fafc',
      borderBottom: '1px solid #e2e8f0',
      opacity: read ? 0.7 : 1,
    }}>
      <NotificationIcon type={type} />

      <div style={{ flex: 1 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between' }}>
          <strong>{title}</strong>
          <span style={{ color: '#a0aec0', fontSize: '0.8rem' }}>{time}</span>
        </div>
        <p style={{ margin: '4px 0 0', color: '#4a5568', fontSize: '0.9rem' }}>
          {message}
        </p>
      </div>

      <div style={{ display: 'flex', gap: '4px' }}>
        {!read && (
          <button
            onClick={onMarkRead}
            style={{
              background: 'none',
              border: 'none',
              cursor: 'pointer',
              color: '#4299e1',
              fontSize: '0.8rem',
            }}
          >
            標為已讀
          </button>
        )}
        <button
          onClick={onDelete}
          style={{
            background: 'none',
            border: 'none',
            cursor: 'pointer',
            color: '#e53e3e',
            fontSize: '0.8rem',
          }}
        >
          刪除
        </button>
      </div>
    </div>
  );
};

// ============================================================
// 通知標頭元件
// ============================================================
const NotificationHeader = ({ total, unreadCount, onClearAll }) => (
  <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: '16px',
    borderBottom: '2px solid #e2e8f0',
  }}>
    <div>
      <h2 style={{ margin: 0 }}>
        通知中心
        {unreadCount > 0 && (
          <span style={{
            marginLeft: '8px',
            padding: '2px 8px',
            borderRadius: '12px',
            backgroundColor: '#e53e3e',
            color: 'white',
            fontSize: '0.8rem',
          }}>
            {unreadCount}
          </span>
        )}
      </h2>
      <p style={{ margin: '4px 0 0', color: '#999', fontSize: '0.85rem' }}>
        共 {total} 則通知
      </p>
    </div>
    <button
      onClick={onClearAll}
      style={{
        padding: '6px 12px',
        border: '1px solid #e2e8f0',
        borderRadius: '6px',
        backgroundColor: 'white',
        cursor: 'pointer',
      }}
    >
      全部清除
    </button>
  </div>
);

// ============================================================
// 通知中心元件(組合所有子元件)
// ============================================================
const NotificationCenter = ({ notifications }) => {
  const unreadCount = notifications.filter((n) => !n.read).length;

  return (
    <div style={{
      maxWidth: '500px',
      margin: '20px auto',
      border: '1px solid #e2e8f0',
      borderRadius: '12px',
      overflow: 'hidden',
      fontFamily: 'sans-serif',
    }}>
      <NotificationHeader
        total={notifications.length}
        unreadCount={unreadCount}
        onClearAll={() => alert('清除所有通知')}
      />

      {notifications.length === 0 ? (
        <div style={{ padding: '40px', textAlign: 'center', color: '#a0aec0' }}>
          沒有通知
        </div>
      ) : (
        notifications.map((notif) => (
          <NotificationItem
            key={notif.id}
            type={notif.type}
            title={notif.title}
            message={notif.message}
            time={notif.time}
            read={notif.read}
            onMarkRead={() => alert(`標記 "${notif.title}" 為已讀`)}
            onDelete={() => alert(`刪除 "${notif.title}"`)}
          />
        ))
      )}
    </div>
  );
};

// ============================================================
// App
// ============================================================
function App() {
  return <NotificationCenter notifications={notifications} />;
}

export default App;

4.9 練習題

練習 1:建立一個 Alert 元件

建立一個接受以下 props 的 Alert 元件: - type:'success' | 'warning' | 'error' | 'info' - title:標題 - children:內容 - closable:是否顯示關閉按鈕 - onClose:關閉時的回呼函式

練習 2:建立一個 Table 元件

建立一個通用的 Table 元件: - columns:欄位定義陣列 [{ key, title, width }] - data:資料陣列 - striped:是否顯示條紋 - bordered:是否顯示邊框


本課重點回顧

  1. Props 是父傳子的單向資料流
  2. Props 是唯讀的,子元件不可修改
  3. 使用解構讓程式碼更簡潔
  4. children 可以傳遞 JSX 到元件內部
  5. 使用展開運算子可以簡化 props 傳遞
  6. 透過傳遞函式實現子對父的通訊
  7. 使用 PropTypesTypeScript 進行型別檢查