6.1 React 事件基礎
React 使用合成事件 (Synthetic Events),提供跨瀏覽器一致的事件 API。
React 事件 vs HTML 事件
<!-- HTML -->
<button onclick="handleClick()">Click</button>
<!-- React JSX -->
<button onClick={handleClick}>Click</button>
| 差異 | HTML | React |
|---|---|---|
| 命名 | 全小寫 onclick |
camelCase onClick |
| 值 | 字串 "handleClick()" |
函式參考 {handleClick} |
| 阻止預設 | return false |
e.preventDefault() |
6.2 事件處理函式的寫法
import { useState } from 'react';
function EventExamples() {
const [message, setMessage] = useState('');
// 方式 1:獨立函式(推薦 — 可複用、好測試)
const handleClick = () => {
setMessage('按鈕被點擊了!');
};
// 方式 2:帶參數的函式
const handleGreet = (name) => {
setMessage(`你好,${name}!`);
};
// 方式 3:接收事件物件
const handleInputChange = (event) => {
setMessage(`輸入了:${event.target.value}`);
};
// 方式 4:同時接收事件物件和自訂參數
const handleItemClick = (itemId, event) => {
setMessage(`點擊了項目 ${itemId},標籤:${event.target.tagName}`);
};
return (
<div style={{ padding: '20px' }}>
{/* 方式 1:直接傳函式參考 */}
<button onClick={handleClick}>點擊我</button>
{/* 方式 2:使用箭頭函式包裝(需要傳參數時) */}
<button onClick={() => handleGreet('Alice')}>打招呼</button>
{/* ❌ 錯誤:這會在渲染時就立刻執行! */}
{/* <button onClick={handleGreet('Alice')}>打招呼</button> */}
{/* 方式 3:事件物件會自動傳入 */}
<input onChange={handleInputChange} placeholder="輸入文字" />
{/* 方式 4:同時傳事件和參數 */}
<button onClick={(e) => handleItemClick(42, e)}>項目 42</button>
{/* 行內處理(簡單邏輯時可用) */}
<button onClick={() => setMessage('行內處理')}>行內</button>
<p>{message}</p>
</div>
);
}
6.3 常用事件類型
滑鼠事件
function MouseEvents() {
const [info, setInfo] = useState('');
return (
<div style={{ padding: '20px' }}>
<div
onClick={() => setInfo('單擊')}
onDoubleClick={() => setInfo('雙擊')}
onMouseEnter={() => setInfo('滑鼠進入')}
onMouseLeave={() => setInfo('滑鼠離開')}
onMouseMove={(e) => setInfo(`座標:(${e.clientX}, ${e.clientY})`)}
onContextMenu={(e) => {
e.preventDefault();
setInfo('右鍵選單被阻止了');
}}
style={{
width: '300px',
height: '200px',
backgroundColor: '#edf2f7',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '8px',
cursor: 'pointer',
userSelect: 'none',
}}
>
在此區域操作滑鼠
</div>
<p>{info}</p>
</div>
);
}
鍵盤事件
function KeyboardEvents() {
const [keys, setKeys] = useState([]);
const handleKeyDown = (e) => {
const keyInfo = `${e.key} (code: ${e.code})`;
// 快捷鍵偵測
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
alert('Ctrl+S 被按下了!');
return;
}
if (e.key === 'Enter') {
alert('Enter 被按下了!');
return;
}
if (e.key === 'Escape') {
setKeys([]);
return;
}
setKeys((prev) => [...prev.slice(-9), keyInfo]);
};
return (
<div style={{ padding: '20px' }}>
<input
onKeyDown={handleKeyDown}
onKeyUp={(e) => console.log('Key up:', e.key)}
placeholder="在這裡按鍵盤..."
style={{ padding: '10px', width: '300px', fontSize: '1rem' }}
/>
<div style={{ marginTop: '12px' }}>
<h3>最近按下的鍵:</h3>
{keys.map((key, i) => (
<span key={i} style={{
display: 'inline-block',
margin: '4px',
padding: '4px 8px',
backgroundColor: '#e2e8f0',
borderRadius: '4px',
fontSize: '0.85rem',
}}>
{key}
</span>
))}
</div>
<p style={{ color: '#999', fontSize: '0.85rem' }}>
提示:試試 Ctrl+S、Enter、Escape
</p>
</div>
);
}
表單事件
function FormEvents() {
const [formData, setFormData] = useState({
username: '',
password: '',
remember: false,
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData((prev) => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
};
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表單預設的頁面重新載入
console.log('提交的資料:', formData);
alert(JSON.stringify(formData, null, 2));
};
const handleReset = () => {
setFormData({ username: '', password: '', remember: false });
};
return (
<form onSubmit={handleSubmit} onReset={handleReset}>
<div>
<label htmlFor="username">帳號:</label>
<input
id="username"
name="username"
value={formData.username}
onChange={handleChange}
onFocus={() => console.log('帳號欄位獲得焦點')}
onBlur={() => console.log('帳號欄位失去焦點')}
/>
</div>
<div>
<label htmlFor="password">密碼:</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
/>
</div>
<div>
<label>
<input
name="remember"
type="checkbox"
checked={formData.remember}
onChange={handleChange}
/>
記住我
</label>
</div>
<button type="submit">登入</button>
<button type="reset">重置</button>
</form>
);
}
6.4 事件傳播 (Event Propagation)
function EventPropagation() {
const handleOuterClick = () => console.log('外層被點擊');
const handleMiddleClick = () => console.log('中層被點擊');
const handleInnerClick = (e) => {
console.log('內層被點擊');
// 如果不想讓事件繼續往上冒泡,可以使用:
// e.stopPropagation();
};
const boxStyle = (color, size) => ({
width: size,
height: size,
backgroundColor: color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
borderRadius: '8px',
});
return (
<div>
<h2>事件冒泡 (Bubbling)</h2>
<p>點擊最內層,觀察 Console 的輸出順序</p>
<div onClick={handleOuterClick} style={boxStyle('#fed7d7', '300px')}>
外層 (red)
<div onClick={handleMiddleClick} style={boxStyle('#fefcbf', '200px')}>
中層 (yellow)
<div onClick={handleInnerClick} style={boxStyle('#c6f6d5', '100px')}>
內層 (green)
</div>
</div>
</div>
<h2>阻止冒泡</h2>
<div
onClick={() => alert('外層')}
style={{ padding: '20px', backgroundColor: '#eee' }}
>
<button onClick={(e) => {
e.stopPropagation();
alert('按鈕(冒泡已被阻止,外層不會觸發)');
}}>
點擊我
</button>
</div>
<h2>阻止預設行為</h2>
<a
href="https://google.com"
onClick={(e) => {
e.preventDefault();
alert('連結被阻止了,不會跳轉');
}}
>
點擊這個連結(不會跳轉)
</a>
</div>
);
}
6.5 合成事件 (Synthetic Event) 詳解
function SyntheticEventDemo() {
const handleClick = (e) => {
// React 的合成事件
console.log('事件類型:', e.type); // 'click'
console.log('目標元素:', e.target); // 被點擊的元素
console.log('當前元素:', e.currentTarget); // 綁定事件的元素
console.log('時間戳:', e.timeStamp);
console.log('是否冒泡:', e.bubbles);
// 滑鼠位置
console.log('clientX:', e.clientX);
console.log('clientY:', e.clientY);
console.log('pageX:', e.pageX);
console.log('pageY:', e.pageY);
// 修飾鍵
console.log('Ctrl:', e.ctrlKey);
console.log('Shift:', e.shiftKey);
console.log('Alt:', e.altKey);
console.log('Meta(Cmd):', e.metaKey);
// 取得原生事件
console.log('原生事件:', e.nativeEvent);
};
return <button onClick={handleClick}>檢查合成事件</button>;
}
6.6 完整範例:互動式畫板
import { useState } from 'react';
function DrawingBoard() {
const [isDrawing, setIsDrawing] = useState(false);
const [dots, setDots] = useState([]);
const [currentColor, setCurrentColor] = useState('#4299e1');
const [dotSize, setDotSize] = useState(10);
const colors = ['#4299e1', '#48bb78', '#ed8936', '#e53e3e', '#805ad5', '#1a1a2e'];
const handleMouseDown = () => setIsDrawing(true);
const handleMouseUp = () => setIsDrawing(false);
const handleMouseLeave = () => setIsDrawing(false);
const handleMouseMove = (e) => {
if (!isDrawing) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setDots((prev) => [...prev, { x, y, color: currentColor, size: dotSize }]);
};
const clearBoard = () => setDots([]);
return (
<div style={{ maxWidth: '600px', margin: '20px auto', fontFamily: 'sans-serif' }}>
<h1 style={{ textAlign: 'center' }}>互動式畫板</h1>
{/* 工具列 */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
marginBottom: '12px',
padding: '12px',
backgroundColor: '#f7fafc',
borderRadius: '8px',
}}>
<span>顏色:</span>
{colors.map((color) => (
<div
key={color}
onClick={() => setCurrentColor(color)}
style={{
width: '28px',
height: '28px',
borderRadius: '50%',
backgroundColor: color,
cursor: 'pointer',
border: currentColor === color ? '3px solid #333' : '2px solid transparent',
}}
/>
))}
<span style={{ marginLeft: '12px' }}>大小:</span>
<input
type="range"
min="3"
max="30"
value={dotSize}
onChange={(e) => setDotSize(Number(e.target.value))}
style={{ width: '80px' }}
/>
<span>{dotSize}px</span>
<button
onClick={clearBoard}
style={{
marginLeft: 'auto',
padding: '6px 16px',
border: '1px solid #e53e3e',
backgroundColor: 'white',
color: '#e53e3e',
borderRadius: '6px',
cursor: 'pointer',
}}
>
清除
</button>
</div>
{/* 畫布 */}
<div
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{
position: 'relative',
width: '100%',
height: '400px',
backgroundColor: 'white',
border: '2px solid #e2e8f0',
borderRadius: '8px',
cursor: 'crosshair',
overflow: 'hidden',
}}
>
{dots.map((dot, i) => (
<div
key={i}
style={{
position: 'absolute',
left: dot.x - dot.size / 2,
top: dot.y - dot.size / 2,
width: dot.size,
height: dot.size,
borderRadius: '50%',
backgroundColor: dot.color,
pointerEvents: 'none',
}}
/>
))}
</div>
<p style={{ textAlign: 'center', color: '#999', fontSize: '0.85rem' }}>
按住滑鼠左鍵在畫布上拖曳來繪畫 | 點數:{dots.length}
</p>
</div>
);
}
export default DrawingBoard;
6.7 練習題
練習 1:拖曳方塊
建立一個可以用滑鼠拖曳移動的方塊元件。
練習 2:快捷鍵系統
建立一個支援以下快捷鍵的應用:
- Ctrl+N:新增項目
- Ctrl+D:刪除選中項目
- Escape:取消選擇
- ↑/↓:在列表中上下移動選擇
本課重點回顧
- React 事件使用 camelCase 命名
- 傳遞函式參考,不是字串
- 事件物件
e是 React 的合成事件 - 使用
e.preventDefault()阻止預設行為 - 使用
e.stopPropagation()阻止冒泡 - 需要傳參數時用箭頭函式包裝:
onClick={() => fn(arg)}