⚛ React
|
非常完整的繁體中文 React 學習平台,從基礎到進階,帶你掌握現代前端開發技術!
專業的 React 繁體中文教學及練習範例網站,提供豐富的React的語法教學及介紹、React安裝教學、React VScode教學和互動式學習教學體驗,助你由入門到專家掌握React編程語言!
什麼是 React?
React 是由 Facebook(Meta)開發的開源 JavaScript 函式庫,專門用於建構使用者介面,採用元件化架構和虛擬 DOM 技術。
元件化設計
將 UI 拆分為獨立、可重用的元件,每個元件管理自己的狀態,組合成複雜的使用者介面,提升開發效率。
虛擬 DOM
React 維護虛擬 DOM 樹,比較差異後只更新真正需要改變的部分,大幅提升渲染效能和使用者體驗。
課程特色
完整繁體中文教學,包含語法高亮、互動範例、知識測驗、學習進度追蹤,讓你高效掌握 React 開發。
學習路線圖
🌱 入門基礎(第 1-4 章)
了解 React 是什麼、安裝開發環境、學習 JSX 語法和虛擬 DOM 原理
⚙️ 核心概念(第 5-12 章)
掌握元件、Props、State、事件處理、條件渲染、列表和表單
🎣 React Hooks(第 13-19 章)
深入學習所有常用 Hooks:useState、useEffect、useContext 等
🚀 進階主題(第 20-23 章)
Context API、React Router、效能優化和錯誤處理
什麼是 React?
React(也稱為 React.js 或 ReactJS)是由 Meta(前身 Facebook)的工程師 Jordan Walke 於 2011 年開發的開源 JavaScript 函式庫,主要用於建構互動式使用者介面(User Interface)。React 在 2013 年 5 月正式開源,迅速成為全球最受歡迎的前端開發工具之一。
React 不是一個完整的框架(Framework),而是一個函式庫(Library),專注於視圖層(View Layer)的渲染。你可以根據需求搭配其他工具使用。
React 的三大核心特點
元件化(Component-Based)
將 UI 拆分成獨立、可重用的小塊(元件),每個元件管理自己的狀態和邏輯,組合成複雜的 UI。
單向資料流(One-Way Data Flow)
資料從父元件流向子元件(透過 Props),使資料流向可預測,易於除錯和維護。
虛擬 DOM(Virtual DOM)
在記憶體中維護虛擬的 DOM 樹,透過差異比較(Diffing)算法只更新真正需要改變的節點。
React 版本歷史
React vs Vue vs Angular 比較
| 特性 | React | Vue | Angular |
|---|---|---|---|
| 類型 | 函式庫 | 漸進式框架 | 完整框架 |
| 開發者 | Meta(Facebook) | 尤雨溪 | |
| 語言 | JSX + JavaScript | HTML模板 + JavaScript | TypeScript |
| 學習曲線 | 中等 | 較易 | 較難 |
| 效能 | 優秀 | 優秀 | 良好 |
| 生態系統 | 龐大 | 豐富 | 完整 |
| 市場佔有率 | 最高 | 第二 | 第三 |
| 企業採用 | Meta, Netflix, Airbnb | 阿里巴巴, GitLab | Google, Microsoft |
在學習 React 之前,請確保你已掌握 HTML、CSS 和 JavaScript(特別是 ES6+ 語法,如箭頭函式、解構賦值、模組化等)。
知識測驗
測試你對 React 簡介的理解
前置需求:安裝 Node.js
React 開發需要 Node.js 環境。請前往 nodejs.org 下載並安裝 LTS 版本(建議 v20 以上)。
# 確認 Node.js 版本(需 v18+)
node --version
# v20.10.0
# 確認 npm 版本
npm --version
# 10.2.3
兩種建立專案的方式
⚡ Vite(推薦)
- 極快的冷啟動速度
- 即時模組熱替換(HMR)
- 現代化工具鏈
- 支援 TypeScript 開箱即用
- 社群最新推薦方式
🔧 Create React App
- 官方工具(較舊)
- 啟動速度較慢
- 設定較為繁瑣
- 社群支援逐漸減少
- 適合學習舊版教材
# 建立新專案(my-app 為專案名稱)
npm create vite@latest my-app -- --template react
# 進入專案目錄
cd my-app
# 安裝依賴套件
npm install
# 啟動開發伺服器
npm run dev
# 使用 npx 建立專案
npx create-react-app my-app
# 進入專案目錄
cd my-app
# 啟動開發伺服器
npm start
Vite 專案結構說明
├── public/ # 靜態資源(不會被處理)
│ └── vite.svg
├── src/ # 主要原始碼目錄
│ ├── assets/ # 圖片等資源
│ ├── App.jsx # 根元件
│ ├── App.css # 根元件樣式
│ ├── main.jsx # 程式入口點
│ └── index.css # 全域樣式
├── index.html # HTML 模板
├── package.json # 專案設定和依賴
├── vite.config.js # Vite 設定檔
└── .gitignore # Git 忽略設定
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
// 找到 HTML 中的 root 元素,並渲染 App 元件
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
StrictMode 是 React 的開發模式工具,它會讓元件在開發時渲染兩次,幫助你發現潛在問題。正式部署時不會有雙重渲染的影響。
什麼是 JSX?
JSX(JavaScript XML)是 React 的語法擴充,讓你可以在 JavaScript 程式碼中直接撰寫類 HTML 結構。JSX 並非瀏覽器原生支援,需要透過 Babel 等工具編譯成標準 JavaScript。
JSX vs 純 JavaScript 對比
✅ 使用 JSX(推薦)
const element = (
<div className="card">
<h1>Hello, React!</h1>
<p>使用 JSX 更直覺</p>
</div>
);
❌ 不使用 JSX
const element = React.createElement(
'div',
{ className: 'card' },
React.createElement('h1', null, 'Hello, React!'),
React.createElement('p', null, '更冗長複雜')
);
JSX 四大規則
JSX 表達式必須有一個外層包裹元素。可以使用 <div> 或 React Fragment <>...</>
// ❌ 錯誤:多個根元素
function Bad() {
return (
<h1>標題</h1>
<p>段落</p> // 語法錯誤!
);
}
// ✅ 正確:使用 Fragment
function Good() {
return (
<>
<h1>標題</h1>
<p>段落</p>
</>
);
}
由於 class 是 JavaScript 保留字,JSX 中必須使用 className
所有 HTML 標籤都必須正確關閉:<img />、<input />、<br />
在 JSX 中用大括號 {} 包裹任何有效的 JavaScript 表達式
function Profile() {
const name = "Alice";
const age = 28;
const skills = ["React", "JavaScript", "CSS"];
return (
<div className="profile">
{/* 這是 JSX 中的注解 */}
<h2>姓名:{name}</h2>
<p>年齡:{age} 歲</p>
<p>{age >= 18 ? "成年" : "未成年"}</p>
<p>技能數量:{skills.length}</p>
<ul>
{skills.map(skill => (
<li key={skill}>{skill}</li>
))}
</ul>
</div>
);
}
function StyledBox() {
const boxStyle = {
backgroundColor: '#1a1a2e',
color: '#00d4ff',
padding: '20px',
borderRadius: '12px',
fontSize: '18px'
};
return (
// style 接受 JavaScript 物件(注意駝峰命名法)
<div style={boxStyle}>
我是一個有樣式的盒子!
</div>
);
}
知識測驗
JSX 語法掌握度測試
什麼是 DOM?
DOM(Document Object Model)是瀏覽器將 HTML 文件轉換成的樹狀物件結構。直接操作 DOM 非常耗效能,因為每次更改都可能觸發瀏覽器的重新排版(Reflow)和重繪(Repaint)。
虛擬 DOM 的運作原理
虛擬 DOM 三步驟
- 第一步 — 渲染:React 將元件轉換為虛擬 DOM 樹(JavaScript 物件)
- 第二步 — 差異比較(Diffing):當狀態改變,React 建立新的虛擬 DOM 並與舊的比對差異
- 第三步 — 協調(Reconciliation):只將有差異的部分更新到真實 DOM
批次更新多次 DOM 操作,避免不必要的重排和重繪。React 的 Fiber 架構(v16+)進一步支援可中斷渲染,提升複雜應用的流暢度。
建立第一個元件
函式元件是一個普通的 JavaScript 函式,接受 props 作為參數,返回 JSX 描述 UI。元件名稱必須以大寫字母開頭。
// 函式元件定義
function Welcome() {
return (
<div>
<h1>你好!歡迎來到 React 世界 🎉</h1>
<p>這是我的第一個元件。</p>
</div>
);
}
// 也可以用箭頭函式寫法
const Welcome = () => {
return <h1>你好,React!</h1>;
};
// 使用元件(像 HTML 標籤一樣)
createRoot(document.getElementById('root')).render(
<Welcome />
);
// 子元件
function Car() {
return <h2>🚗 我是一台車!</h2>;
}
// 父元件包含子元件
function Garage() {
return (
<div>
<h1>🏠 我的車庫裡有什麼?</h1>
<Car /> {/* 使用 Car 元件 */}
<Car /> {/* 可以重複使用 */}
<Car />
</div>
);
}
// 渲染最外層元件
createRoot(document.getElementById('root')).render(
<Garage />
);
元件名稱必須大寫。小寫被視為原生 HTML 標籤(如 <div>),大寫才是自訂元件(如 <MyComponent />)。
元件匯出與匯入
// Button.jsx — 獨立元件檔案
function Button({ label, onClick, variant = 'primary' }) {
const styles = {
primary: { background: '#00d4ff', color: '#000' },
danger: { background: '#ef4444', color: '#fff' },
};
return (
<button
onClick={onClick}
style={styles[variant]}
>
{label}
</button>
);
}
export default Button; // 匯出元件
import Button from './Button'; // 匯入元件
function App() {
return (
<div>
<Button label="確定" variant="primary" onClick={() => alert('點擊!')} />
<Button label="刪除" variant="danger" onClick={() => alert('刪除!')} />
</div>
);
}
類別元件基本結構
在 Hooks 出現之前,類別元件是使用狀態和生命週期的唯一方式。現在建議優先使用函式元件 + Hooks,但理解類別元件仍很重要。
import { Component } from 'react';
class Counter extends Component {
// 建構子:初始化 state
constructor(props) {
super(props); // 必須呼叫 super(props)
this.state = {
count: 0
};
}
// 生命週期:元件掛載後執行
componentDidMount() {
console.log('元件已掛載!');
}
// 生命週期:元件更新後執行
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
console.log(`計數更新為:${this.state.count}`);
}
}
// 生命週期:元件卸載前執行
componentWillUnmount() {
console.log('元件即將卸載!');
}
render() {
return (
<div>
<h2>計數:{this.state.count}</h2>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
+1
</button>
</div>
);
}
}
生命週期方法對比
| 類別元件 | 函式元件 (Hooks) | 說明 |
|---|---|---|
componentDidMount | useEffect(fn, []) | 元件首次渲染後執行 |
componentDidUpdate | useEffect(fn, [deps]) | 依賴更新時執行 |
componentWillUnmount | useEffect(() => { return cleanup }, []) | 元件卸載前清除 |
this.setState | useState | 狀態管理 |
React 官方目前推薦使用函式元件 + Hooks 取代類別元件。Hooks 更簡潔、更易於測試和複用邏輯。
Props 基本概念
Props(Properties)是 React 元件的輸入參數,從父元件傳遞到子元件。Props 是唯讀的,子元件不應修改它們。
// 子元件接受 props
function UserCard(props) {
return (
<div className="card">
<h2>{props.name}</h2>
<p>年齡:{props.age}</p>
<p>職業:{props.job}</p>
</div>
);
}
// 父元件傳遞 props
function App() {
return (
<div>
<UserCard name="Alice" age={28} job="前端工程師" />
<UserCard name="Bob" age={32} job="後端工程師" />
</div>
);
}
// 使用解構賦值讓程式碼更簡潔
function Card({ title, children, color = 'blue', size = 'medium' }) {
const sizeMap = {
small: '14px',
medium: '16px',
large: '20px'
};
return (
<div style={{
borderLeft: `4px solid ${color}`,
padding: '16px',
fontSize: sizeMap[size]
}}>
<h3>{title}</h3>
<div>{children}</div>
</div>
);
}
// 使用元件
function App() {
return (
<div>
<Card title="資訊卡片" color="#00d4ff">
<p>這是卡片的子內容(children props)</p>
</Card>
<Card title="警告卡片" color="#ef4444" size="large">
<p>這是大尺寸的警告卡片</p>
</Card>
</div>
);
}
// 子元件接受函式 prop(回調函式)
function DeleteButton({ onDelete, itemName }) {
return (
<button
onClick={() => onDelete(itemName)}
style={{ color: 'red' }}
>
刪除 {itemName}
</button>
);
}
// 父元件提供處理函式
function TodoList() {
const handleDelete = (name) => {
alert(`已刪除:${name}`);
};
return (
<div>
<DeleteButton onDelete={handleDelete} itemName="任務A" />
<DeleteButton onDelete={handleDelete} itemName="任務B" />
</div>
);
}
永遠不要在子元件中修改 props。如果需要更改資料,應該將狀態提升到父元件,並透過回調函式更新。
知識測驗
Props 傳遞概念測試
State vs Props 比較
| 特性 | State | Props |
|---|---|---|
| 來源 | 元件內部 | 父元件傳入 |
| 可修改 | 可以(用 setState) | 不可以(唯讀) |
| 用途 | 管理元件的動態資料 | 接收外部資料 |
| 更新觸發 | 重新渲染元件 | 父元件重新渲染 |
import { useState } from 'react';
function Counter() {
// useState 返回 [當前值, 更新函式]
const [count, setCount] = useState(0);
return (
<div>
<h2>計數:{count}</h2>
<button onClick={() => setCount(count + 1)}>➕ 增加</button>
<button onClick={() => setCount(count - 1)}>➖ 減少</button>
<button onClick={() => setCount(0)}>🔄 重置</button>
</div>
);
}
import { useState } from 'react';
function UserProfile() {
const [user, setUser] = useState({
name: 'Alice',
age: 28,
email: '[email protected]'
});
const updateEmail = () => {
// 使用展開運算子保留其他欄位
setUser(prev => ({
...prev,
email: '[email protected]'
}));
};
return (
<div>
<p>姓名:{user.name}</p>
<p>年齡:{user.age}</p>
<p>Email:{user.email}</p>
<button onClick={updateEmail}>更新 Email</button>
</div>
);
}
import { useState } from 'react';
function TodoApp() {
const [todos, setTodos] = useState(['學習 React', '練習 JSX']);
const [input, setInput] = useState('');
const addTodo = () => {
if (!input.trim()) return;
setTodos(prev => [...prev, input]); // 不要直接修改原陣列!
setInput('');
};
const removeTodo = (index) => {
setTodos(prev => prev.filter((_, i) => i !== index));
};
return (
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="新增待辦事項..."
/>
<button onClick={addTodo}>新增</button>
<ul>
{todos.map((todo, i) => (
<li key={i}>
{todo}
<button onClick={() => removeTodo(i)}>❌</button>
</li>
))}
</ul>
</div>
);
}
不要直接修改 state:todos.push('新項目') ❌
要建立新陣列:setTodos([...todos, '新項目']) ✅
React 需要接收新物件/陣列來偵測到變化並觸發重新渲染。
React 事件基礎
React 的事件處理使用駝峰命名法(camelCase),並傳入函式而非字串。
import { useState } from 'react';
function EventDemo() {
const [message, setMessage] = useState('');
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
// 點擊事件
const handleClick = (e) => {
setMessage(`按鈕被點擊!時間:${new Date().toLocaleTimeString()}`);
};
// 鍵盤事件
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
setMessage(`按下 Enter!輸入值:${e.target.value}`);
}
};
// 滑鼠移動事件
const handleMouseMove = (e) => {
setMousePos({ x: e.clientX, y: e.clientY });
};
// 阻止默認行為
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表單提交頁面跳轉
setMessage('表單已提交(頁面不重整)');
};
return (
<div onMouseMove={handleMouseMove}>
<button onClick={handleClick}>點我</button>
<input onKeyPress={handleKeyPress} placeholder="按 Enter 觸發事件" />
<form onSubmit={handleSubmit}>
<button type="submit">提交表單</button>
</form>
<p>{message}</p>
<p>滑鼠位置:X:{mousePos.x}, Y:{mousePos.y}</p>
</div>
);
}
三種條件渲染方式
import { useState } from 'react';
function ConditionalDemo() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [role, setRole] = useState('user');
const [count, setCount] = useState(0);
return (
<div>
{/* 方式一:if/else(在 JSX 外使用) */}
{(() => {
if (isLoggedIn) return <p>✅ 歡迎回來!</p>;
else return <p>❌ 請先登入</p>;
})()}
{/* 方式二:三元運算子(最常用) */}
<h3>{isLoggedIn ? '已登入' : '未登入'}</h3>
{/* 方式三:邏輯 && 運算子 */}
{isLoggedIn && <p>🎉 你是登入用戶!</p>}
{count > 0 && <p>計數:{count}</p>}
{/* 方式四:根據角色渲染不同元件 */}
{{
admin: <span>👑 管理員介面</span>,
user: <span>👤 一般用戶介面</span>,
guest: <span>👻 訪客介面</span>
}[role]}
<button onClick={() => setIsLoggedIn(!isLoggedIn)}>
切換登入狀態
</button>
</div>
);
}
使用 map() 渲染列表
function ProductList() {
const products = [
{ id: 1, name: 'iPhone 15', price: 29900, stock: true },
{ id: 2, name: 'MacBook Pro', price: 59900, stock: false },
{ id: 3, name: 'iPad Air', price: 19900, stock: true },
];
return (
<ul>
{products.map(product => (
// key 必須是唯一且穩定的值,使用 id 而非 index
<li key={product.id}>
<strong>{product.name}</strong> - NT${product.price}
<span>{product.stock ? '✅ 有庫存' : '❌ 缺貨'}</span>
</li>
))}
</ul>
);
}
Key 幫助 React 識別哪些項目被新增、修改或刪除。使用穩定的唯一 ID 作為 key,避免使用陣列 index(除非列表不會排序或篩選)。
受控元件(Controlled Component)
import { useState } from 'react';
function RegisterForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
role: 'user',
agreed: 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);
};
return (
<form onSubmit={handleSubmit}>
<input
name="username"
value={formData.username}
onChange={handleChange}
placeholder="使用者名稱"
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="電子郵件"
/>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="密碼"
/>
<select name="role" value={formData.role} onChange={handleChange}>
<option value="user">一般用戶</option>
<option value="admin">管理員</option>
</select>
<label>
<input
name="agreed"
type="checkbox"
checked={formData.agreed}
onChange={handleChange}
/>
我同意服務條款
</label>
<button type="submit" disabled={!formData.agreed}>
註冊
</button>
</form>
);
}
useState 深入解析
import { useState } from 'react';
function UseStateDemo() {
// 基本用法:初始值為 0
const [count, setCount] = useState(0);
// 函式初始值(昂貴計算只執行一次)
const [data, setData] = useState(() => {
const saved = localStorage.getItem('data');
return saved ? JSON.parse(saved) : { value: 0 };
});
// 使用函式更新(確保基於最新 state)
const increment = () => {
// ❌ 這樣可能不準確(批次更新問題)
// setCount(count + 1);
// ✅ 函式式更新(推薦)
setCount(prev => prev + 1);
};
// 多次更新
const incrementBy3 = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return (
<div>
<p>計數:{count}</p>
<button onClick={increment}>+1</button>
<button onClick={incrementBy3}>+3</button>
</div>
);
}
1. 只在函式元件的最頂層呼叫 Hook(不在迴圈或條件中)
2. 只在 React 函式元件或自訂 Hook 中呼叫
useEffect 三種使用模式
import { useState, useEffect } from 'react';
// ① 無依賴陣列 — 每次渲染後都執行
useEffect(() => {
console.log('每次渲染後都執行');
});
// ② 空依賴陣列 — 只在掛載時執行一次
useEffect(() => {
console.log('只在元件掛載時執行(類似 componentDidMount)');
}, []);
// ③ 有依賴 — 依賴改變時執行
useEffect(() => {
console.log(`count 改變了:${count}`);
}, [count]); // 只在 count 改變時執行
import { useState, useEffect } from 'react';
function UserFetcher({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 用 AbortController 取消過時的請求
const controller = new AbortController();
const fetchUser = async () => {
try {
setLoading(true);
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`,
{ signal: controller.signal }
);
if (!res.ok) throw new Error('載入失敗');
const data = await res.json();
setUser(data);
} catch (err) {
if (err.name !== 'AbortError') setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
// 清除函式:元件卸載或 userId 改變時取消請求
return () => controller.abort();
}, [userId]);
if (loading) return <p>⏳ 載入中...</p>;
if (error) return <p>❌ 錯誤:{error}</p>;
if (!user) return null;
return (
<div>
<h2>{user.name}</h2>
<p>📧 {user.email}</p>
<p>🌐 {user.website}</p>
</div>
);
}
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
if (!isRunning) return; // 不在執行中則跳過
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// 清除函式 — 停止計時器
return () => clearInterval(interval);
}, [isRunning]); // isRunning 改變時重新執行
return (
<div>
<h2>⏱ {seconds} 秒</h2>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? '暫停' : '開始'}
</button>
<button onClick={() => { setSeconds(0); setIsRunning(false); }}>
重置
</button>
</div>
);
}
知識測驗
useEffect 掌握度測試
什麼是 Props Drilling?
當需要將資料從頂層元件傳遞到深層子元件時,必須透過中間層層傳遞 props,這稱為 Props Drilling,非常麻煩。useContext 解決了這個問題。
import { createContext, useContext, useState } from 'react';
// 1. 建立 Context
const ThemeContext = createContext('light');
const UserContext = createContext(null);
// 2. Provider 提供資料
function App() {
const [theme, setTheme] = useState('dark');
const [user] = useState({ name: 'Alice', role: 'admin' });
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
// 3. 深層子元件直接使用 Context(無需 props drilling)
function UserAvatar() {
const { theme } = useContext(ThemeContext);
const user = useContext(UserContext);
return (
<div style={{ background: theme === 'dark' ? '#1a1a2e' : '#fff' }}>
<span>👤 {user.name}({user.role})</span>
</div>
);
}
useRef 兩大用途
import { useRef, useEffect, useState } from 'react';
// 用途一:存取 DOM 元素
function FocusInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus(); // 直接操作 DOM
};
return (
<div>
<input ref={inputRef} placeholder="點擊按鈕自動聚焦" />
<button onClick={focusInput}>聚焦輸入框</button>
</div>
);
}
// 用途二:保存不觸發重渲染的值(例如計時器 ID)
function PersistentTimer() {
const [count, setCount] = useState(0);
const timerRef = useRef(null); // 不觸發重渲染
const start = () => {
timerRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
};
const stop = () => {
clearInterval(timerRef.current);
};
return (
<div>
<p>秒數:{count}</p>
<button onClick={start}>開始</button>
<button onClick={stop}>停止</button>
</div>
);
}
import { useState, useMemo } from 'react';
function ExpensiveCalc() {
const [count, setCount] = useState(0);
const [theme, setTheme] = useState('dark');
// 沒有 useMemo:每次渲染都重新計算
// const result = slowCalculation(count); // 🐢 慢
// 使用 useMemo:只在 count 改變時重新計算
const result = useMemo(() => {
console.log('重新計算...');
// 模擬昂貴計算
let sum = 0;
for (let i = 0; i < count * 1000000; i++) sum += i;
return sum;
}, [count]); // 只有 count 改變時才重新計算
return (
<div>
<p>計算結果:{result}</p>
<button onClick={() => setCount(c => c + 1)}>增加計數(觸發重算)</button>
<button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
切換主題(不重算)
</button>
</div>
);
}
不要濫用 useMemo。只在確定有效能問題時才使用,否則 memo 本身也有開銷。先測量,再優化。
import { useState, useCallback, memo } from 'react';
// 子元件用 memo 包裹,避免不必要重渲染
const Button = memo(function Button({ onClick, label }) {
console.log(`${label} 按鈕重新渲染`);
return <button onClick={onClick}>{label}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
// 沒有 useCallback:每次渲染都建立新函式,導致子元件重渲染
// const handleIncrement = () => setCount(c => c + 1);
// 使用 useCallback:函式引用穩定,子元件不重渲染
const handleIncrement = useCallback(() => {
setCount(c => c + 1);
}, []); // 空陣列 = 函式永不改變
return (
<div>
<p>計數:{count},其他:{other}</p>
<Button onClick={handleIncrement} label="增加計數" />
<button onClick={() => setOther(o => o + 1)}>改變其他(不重渲 Button)</button>
</div>
);
}
建立自訂 Hook
自訂 Hook 是以 use 開頭的函式,內部可以呼叫其他 Hooks。它讓你將複雜邏輯抽離元件,實現邏輯複用。
// 自訂 Hook:同步到 LocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setStoredValue = (newValue) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
};
return [value, setStoredValue];
}
// 自訂 Hook:視窗寬度偵測
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () =>
setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// 自訂 Hook:Fetch 資料
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(r => r.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// 在元件中使用自訂 Hooks
function MyComponent() {
const [name, setName] = useLocalStorage('user-name', '');
const { width } = useWindowSize();
const { data, loading } = useFetch('/api/posts');
return (
<div>
<p>視窗寬度:{width}px > {width > 768 ? '桌機' : '手機'}</p>
<input value={name} onChange={e => setName(e.target.value)} />
{loading ? <p>載入中...</p> : <p>資料數量:{data?.length}</p>}
</div>
);
}
import { createContext, useContext, useReducer } from 'react';
// 建立 Context
const StoreContext = createContext();
// Reducer 函式
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT': return { ...state, count: state.count + 1 };
case 'SET_USER': return { ...state, user: action.payload };
case 'TOGGLE_THEME':
return { ...state, theme: state.theme === 'dark' ? 'light' : 'dark' };
default: return state;
}
}
// Store Provider
function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, {
count: 0,
user: null,
theme: 'dark'
});
return (
<StoreContext.Provider value={{ state, dispatch }}>
{children}
</StoreContext.Provider>
);
}
// 自訂 Hook 封裝
function useStore() {
const context = useContext(StoreContext);
if (!context) throw new Error('useStore 必須在 StoreProvider 內使用');
return context;
}
// 使用
function Counter() {
const { state, dispatch } = useStore();
return (
<div>
<p>計數:{state.count},主題:{state.theme}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
<button onClick={() => dispatch({ type: 'TOGGLE_THEME' })}>切換主題</button>
</div>
);
}
安裝與基本設定
npm install react-router-domimport { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from 'react-router-dom';
// 頁面元件
function Home() { return <h1>🏠 首頁</h1>; }
function About() { return <h1>📋 關於我們</h1>; }
function NotFound() { return <h1>❌ 404 頁面不存在</h1>; }
// 動態路由元件
function UserProfile() {
const { id } = useParams(); // 取得 URL 參數
const navigate = useNavigate(); // 程式化導航
return (
<div>
<h2>用戶 ID:{id}</h2>
<button onClick={() => navigate('/users')}>返回列表</button>
<button onClick={() => navigate(-1)}>上一頁</button>
</div>
);
}
// 導航列
function Navbar() {
return (
<nav>
<Link to="/">首頁</Link>
<Link to="/about">關於</Link>
<Link to="/users/1">用戶1</Link>
</nav>
);
}
// 主應用
function App() {
return (
<BrowserRouter>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<UserProfile />} />
<Route path="*" element={<NotFound />} /> {/* 404 */}
</Routes>
</BrowserRouter>
);
}
效能優化技巧總覽
import { lazy, Suspense } from 'react';
// 懶加載元件(只在需要時載入)
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));
function App() {
return (
<Suspense fallback={<div>⏳ 載入中...</div>}>
<HeavyChart />
</Suspense>
);
}
import { Component } from 'react';
// 錯誤邊界(目前只能用類別元件實作)
class ErrorBoundary extends Component {
state = { hasError: false, error: null };
// 靜態方法:捕獲錯誤,更新 state
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// 記錄錯誤到監控服務
componentDidCatch(error, errorInfo) {
console.error('元件錯誤:', error, errorInfo);
// 可以傳送到 Sentry 等錯誤追蹤服務
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '20px', color: 'red', border: '1px solid red' }}>
<h2>😢 發生錯誤了!</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
🔄 重試
</button>
</div>
);
}
return this.props.children;
}
}
// 使用方式:包裹可能出錯的元件
function App() {
return (
<ErrorBoundary>
<RiskyComponent /> {/* 如果這裡出錯,只影響這個區域 */}
</ErrorBoundary>
);
}
你已完成所有 23 個章節的學習!繼續練習、動手做專案,是成為 React 高手的最佳途徑。