React v19.0.0 最新版

⚛ React
|

非常完整的繁體中文 React 學習平台,從基礎到進階,帶你掌握現代前端開發技術!

專業的 React 繁體中文教學及練習範例網站,提供豐富的React的語法教學及介紹、React安裝教學、React VScode教學和互動式學習教學體驗,助你由入門到專家掌握React編程語言!

23
完整章節
60+
程式碼範例
7
React Hooks
v19
最新版本

什麼是 React?

React 是由 Facebook(Meta)開發的開源 JavaScript 函式庫,專門用於建構使用者介面,採用元件化架構和虛擬 DOM 技術。

元件化設計

將 UI 拆分為獨立、可重用的元件,每個元件管理自己的狀態,組合成複雜的使用者介面,提升開發效率。

虛擬 DOM

React 維護虛擬 DOM 樹,比較差異後只更新真正需要改變的部分,大幅提升渲染效能和使用者體驗。

課程特色

完整繁體中文教學,包含語法高亮、互動範例、知識測驗、學習進度追蹤,讓你高效掌握 React 開發。

學習路線圖

1

🌱 入門基礎(第 1-4 章)

了解 React 是什麼、安裝開發環境、學習 JSX 語法和虛擬 DOM 原理

2

⚙️ 核心概念(第 5-12 章)

掌握元件、Props、State、事件處理、條件渲染、列表和表單

3

🎣 React Hooks(第 13-19 章)

深入學習所有常用 Hooks:useState、useEffect、useContext 等

4

🚀 進階主題(第 20-23 章)

Context API、React Router、效能優化和錯誤處理

01
入門基礎

React 簡介

了解 React 的起源、核心概念與為什麼它成為最流行的前端框架

什麼是 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 版本歷史

2011
Facebook 內部開始使用 React 建構 Newsfeed 功能
2013 年 5 月
React v0.3.0 正式開源於 JSConf US
2015
React v0.14 — 分離 react 和 react-dom 套件;React Native 發布
2016
React v15 — 正式穩定版本,改善 DOM 渲染效能
2017
React v16(Fiber 架構)— 全新核心渲染引擎,支援錯誤邊界
2019
React v16.8 — 引入 Hooks(useState、useEffect 等),革命性改變
2022
React v18 — 並發模式(Concurrent Mode)、自動批次更新、Suspense 改進
2024 年 12 月
React v19 — Server Components 正式穩定、新的 use() Hook、Actions API

React vs Vue vs Angular 比較

特性 React Vue Angular
類型函式庫漸進式框架完整框架
開發者Meta(Facebook)尤雨溪Google
語言JSX + JavaScriptHTML模板 + JavaScriptTypeScript
學習曲線中等較易較難
效能優秀優秀良好
生態系統龐大豐富完整
市場佔有率最高第二第三
企業採用Meta, Netflix, Airbnb阿里巴巴, GitLabGoogle, Microsoft
學習建議

在學習 React 之前,請確保你已掌握 HTML、CSS 和 JavaScript(特別是 ES6+ 語法,如箭頭函式、解構賦值、模組化等)。

🧠

知識測驗

測試你對 React 簡介的理解

02
入門基礎

環境安裝與設定

從零開始建立 React 開發環境,選擇最適合你的工具鏈

前置需求:安裝 Node.js

React 開發需要 Node.js 環境。請前往 nodejs.org 下載並安裝 LTS 版本(建議 v20 以上)。

驗證安裝
Bash
# 確認 Node.js 版本(需 v18+)
node --version
# v20.10.0

# 確認 npm 版本
npm --version
# 10.2.3

兩種建立專案的方式

⚡ Vite(推薦)

  • 極快的冷啟動速度
  • 即時模組熱替換(HMR)
  • 現代化工具鏈
  • 支援 TypeScript 開箱即用
  • 社群最新推薦方式

🔧 Create React App

  • 官方工具(較舊)
  • 啟動速度較慢
  • 設定較為繁瑣
  • 社群支援逐漸減少
  • 適合學習舊版教材
方法一:使用 Vite(推薦)
Bash
# 建立新專案(my-app 為專案名稱)
npm create vite@latest my-app -- --template react

# 進入專案目錄
cd my-app

# 安裝依賴套件
npm install

# 啟動開發伺服器
npm run dev
方法二:Create React App
Bash
# 使用 npx 建立專案
npx create-react-app my-app

# 進入專案目錄
cd my-app

# 啟動開發伺服器
npm start

Vite 專案結構說明

my-app/
├── public/ # 靜態資源(不會被處理)
│ └── vite.svg
├── src/ # 主要原始碼目錄
│ ├── assets/ # 圖片等資源
│ ├── App.jsx # 根元件
│ ├── App.css # 根元件樣式
│ ├── main.jsx # 程式入口點
│ └── index.css # 全域樣式
├── index.html # HTML 模板
├── package.json # 專案設定和依賴
├── vite.config.js # Vite 設定檔
└── .gitignore # Git 忽略設定
main.jsx — 程式入口
JSX
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 的開發模式工具,它會讓元件在開發時渲染兩次,幫助你發現潛在問題。正式部署時不會有雙重渲染的影響。

03
入門基礎

JSX 語法

JavaScript XML — 讓你在 JavaScript 中撰寫類似 HTML 的語法

什麼是 JSX?

JSX(JavaScript XML)是 React 的語法擴充,讓你可以在 JavaScript 程式碼中直接撰寫類 HTML 結構。JSX 並非瀏覽器原生支援,需要透過 Babel 等工具編譯成標準 JavaScript。

JSX vs 純 JavaScript 對比

✅ 使用 JSX(推薦)

JSX
const element = (
  <div className="card">
    <h1>Hello, React!</h1>
    <p>使用 JSX 更直覺</p>
  </div>
);

❌ 不使用 JSX

JavaScript
const element = React.createElement(
  'div',
  { className: 'card' },
  React.createElement('h1', null, 'Hello, React!'),
  React.createElement('p', null, '更冗長複雜')
);

JSX 四大規則

規則一:只能有一個根元素

JSX 表達式必須有一個外層包裹元素。可以使用 <div> 或 React Fragment <>...</>

範例:Fragment 用法
JSX
// ❌ 錯誤:多個根元素
function Bad() {
  return (
    <h1>標題</h1>
    <p>段落</p>    // 語法錯誤!
  );
}

// ✅ 正確:使用 Fragment
function Good() {
  return (
    <>
      <h1>標題</h1>
      <p>段落</p>
    </>
  );
}
規則二:className 替代 class

由於 class 是 JavaScript 保留字,JSX 中必須使用 className

規則三:標籤必須自閉合

所有 HTML 標籤都必須正確關閉:<img /><input /><br />

規則四:用 {} 嵌入 JavaScript 表達式

在 JSX 中用大括號 {} 包裹任何有效的 JavaScript 表達式

範例:JSX 中的表達式
JSX
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>
  );
}
範例:JSX 中的行內樣式
JSX
function StyledBox() {
  const boxStyle = {
    backgroundColor: '#1a1a2e',
    color: '#00d4ff',
    padding: '20px',
    borderRadius: '12px',
    fontSize: '18px'
  };

  return (
    // style 接受 JavaScript 物件(注意駝峰命名法)
    <div style={boxStyle}>
      我是一個有樣式的盒子!
    </div>
  );
}
🧠

知識測驗

JSX 語法掌握度測試

04
入門基礎

虛擬 DOM 概念

理解 React 如何透過虛擬 DOM 實現高效渲染

什麼是 DOM?

DOM(Document Object Model)是瀏覽器將 HTML 文件轉換成的樹狀物件結構。直接操作 DOM 非常耗效能,因為每次更改都可能觸發瀏覽器的重新排版(Reflow)和重繪(Repaint)。

虛擬 DOM 的運作原理

🖥️ 真實 DOM(更新前)
<div>
<h1>
<ul>
<li>A
<li>B
⇨ diff ⇨
⚡ 虛擬 DOM(差異比較)
<div>
<h1>
<ul>
<li>A
<li>C ✓

虛擬 DOM 三步驟

  • 第一步 — 渲染:React 將元件轉換為虛擬 DOM 樹(JavaScript 物件)
  • 第二步 — 差異比較(Diffing):當狀態改變,React 建立新的虛擬 DOM 並與舊的比對差異
  • 第三步 — 協調(Reconciliation):只將有差異的部分更新到真實 DOM
效能優勢

批次更新多次 DOM 操作,避免不必要的重排和重繪。React 的 Fiber 架構(v16+)進一步支援可中斷渲染,提升複雜應用的流暢度。

05
核心概念

函式元件

React 的基本構建單位,學習如何建立可重用的 UI 元件

建立第一個元件

函式元件是一個普通的 JavaScript 函式,接受 props 作為參數,返回 JSX 描述 UI。元件名稱必須以大寫字母開頭

範例一:最簡單的元件
JSX
// 函式元件定義
function Welcome() {
  return (
    <div>
      <h1>你好!歡迎來到 React 世界 🎉</h1>
      <p>這是我的第一個元件。</p>
    </div>
  );
}

// 也可以用箭頭函式寫法
const Welcome = () => {
  return <h1>你好,React!</h1>;
};

// 使用元件(像 HTML 標籤一樣)
createRoot(document.getElementById('root')).render(
  <Welcome />
);
範例二:元件組合
JSX
// 子元件
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 />)。

元件匯出與匯入

JSX — Button.jsx
// 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; // 匯出元件
JSX — App.jsx
import Button from './Button'; // 匯入元件

function App() {
  return (
    <div>
      <Button label="確定" variant="primary" onClick={() => alert('點擊!')} />
      <Button label="刪除" variant="danger" onClick={() => alert('刪除!')} />
    </div>
  );
}
06
核心概念

類別元件

了解傳統類別元件的寫法及其生命週期方法

類別元件基本結構

在 Hooks 出現之前,類別元件是使用狀態和生命週期的唯一方式。現在建議優先使用函式元件 + Hooks,但理解類別元件仍很重要。

JSX
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)說明
componentDidMountuseEffect(fn, [])元件首次渲染後執行
componentDidUpdateuseEffect(fn, [deps])依賴更新時執行
componentWillUnmountuseEffect(() => { return cleanup }, [])元件卸載前清除
this.setStateuseState狀態管理
現代建議

React 官方目前推薦使用函式元件 + Hooks 取代類別元件。Hooks 更簡潔、更易於測試和複用邏輯。

07
核心概念

Props 屬性傳遞

從父元件向子元件傳遞資料的核心機制

Props 基本概念

Props(Properties)是 React 元件的輸入參數,從父元件傳遞到子元件。Props 是唯讀的,子元件不應修改它們。

範例一:基本 Props 傳遞
JSX
// 子元件接受 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>
  );
}
範例二:Props 解構
JSX
// 使用解構賦值讓程式碼更簡潔
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>
  );
}
範例三:傳遞函式作為 Props
JSX
// 子元件接受函式 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。如果需要更改資料,應該將狀態提升到父元件,並透過回調函式更新。

🧠

知識測驗

Props 傳遞概念測試

08
核心概念

State 狀態管理

元件的內部記憶體,讓 UI 能夠動態回應使用者互動

State vs Props 比較

特性StateProps
來源元件內部父元件傳入
可修改可以(用 setState)不可以(唯讀)
用途管理元件的動態資料接收外部資料
更新觸發重新渲染元件父元件重新渲染
範例一:計數器
JSX
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>
  );
}
範例二:物件狀態更新
JSX
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>
  );
}
範例三:陣列狀態更新
JSX
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 需要接收新物件/陣列來偵測到變化並觸發重新渲染。

09
核心概念

事件處理

響應使用者操作:點擊、輸入、提交等互動事件

React 事件基礎

React 的事件處理使用駝峰命名法(camelCase),並傳入函式而非字串。

JSX
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>
  );
}
10
核心概念

條件渲染

根據條件動態顯示或隱藏元件內容

三種條件渲染方式

JSX
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>
  );
}
11
核心概念

列表與 Key

高效渲染動態列表,理解 Key 的重要性

使用 map() 渲染列表

JSX
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 的重要性

Key 幫助 React 識別哪些項目被新增、修改或刪除。使用穩定的唯一 ID 作為 key,避免使用陣列 index(除非列表不會排序或篩選)。

12
核心概念

表單處理

受控元件與非受控元件,實作互動式表單

受控元件(Controlled Component)

JSX
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>
  );
}
13
React Hooks

useState Hook

管理元件內部狀態的核心 Hook

useState 深入解析

JSX
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>
  );
}
Hooks 規則

1. 只在函式元件的最頂層呼叫 Hook(不在迴圈或條件中)
2. 只在 React 函式元件或自訂 Hook 中呼叫

14
React Hooks

useEffect Hook

處理副作用:API 請求、訂閱、計時器、DOM 操作

useEffect 三種使用模式

JSX
import { useState, useEffect } from 'react';

// ① 無依賴陣列 — 每次渲染後都執行
useEffect(() => {
  console.log('每次渲染後都執行');
});

// ② 空依賴陣列 — 只在掛載時執行一次
useEffect(() => {
  console.log('只在元件掛載時執行(類似 componentDidMount)');
}, []);

// ③ 有依賴 — 依賴改變時執行
useEffect(() => {
  console.log(`count 改變了:${count}`);
}, [count]); // 只在 count 改變時執行
範例:API 請求
JSX
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>
  );
}
範例:計時器與清除
JSX
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 掌握度測試

15
React Hooks

useContext Hook

跨元件層級共享資料,解決 Props Drilling 問題

什麼是 Props Drilling?

當需要將資料從頂層元件傳遞到深層子元件時,必須透過中間層層傳遞 props,這稱為 Props Drilling,非常麻煩。useContext 解決了這個問題。

JSX
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>
  );
}
16
React Hooks

useRef Hook

存取 DOM 元素及保存不觸發重渲染的可變值

useRef 兩大用途

JSX
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>
  );
}
17
React Hooks

useMemo Hook

記憶化計算結果,避免昂貴計算的重複執行

JSX
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 本身也有開銷。先測量,再優化。

18
React Hooks

useCallback Hook

記憶化函式,防止子元件不必要的重渲染

JSX
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>
  );
}
19
React Hooks

自訂 Hooks

封裝可複用的狀態邏輯,提升程式碼整潔性

建立自訂 Hook

自訂 Hook 是以 use 開頭的函式,內部可以呼叫其他 Hooks。它讓你將複雜邏輯抽離元件,實現邏輯複用。

JSX — useLocalStorage.js
// 自訂 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>
  );
}
20
進階主題

Context API

全域狀態管理,無需第三方函式庫

JSX
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>
  );
}
21
進階主題

React Router

建立多頁面 SPA,實現客戶端路由導航

安裝與基本設定

Bash
npm install react-router-dom
JSX
import { 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>
  );
}
22
進階主題

效能優化

讓 React 應用更快、更流暢

效能優化技巧總覽

React.memo
防止 props 未改變時的不必要重渲染
useMemo
記憶化昂貴計算結果
useCallback
記憶化函式引用
lazy + Suspense
程式碼分割,按需載入
虛擬化列表
大量資料只渲染可見部分
圖片優化
懶加載、WebP 格式
JSX — 程式碼分割
import { lazy, Suspense } from 'react';

// 懶加載元件(只在需要時載入)
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));

function App() {
  return (
    <Suspense fallback={<div>⏳ 載入中...</div>}>
      <HeavyChart />
    </Suspense>
  );
}
23
進階主題

錯誤邊界

優雅處理元件錯誤,防止整個應用崩潰

JSX
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 高手的最佳途徑。