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:是否顯示邊框
本課重點回顧
- Props 是父傳子的單向資料流
- Props 是唯讀的,子元件不可修改
- 使用解構讓程式碼更簡潔
children可以傳遞 JSX 到元件內部- 使用展開運算子可以簡化 props 傳遞
- 透過傳遞函式實現子對父的通訊
- 使用 PropTypes 或 TypeScript 進行型別檢查