CHAPTER 06 / FRAMEWORK
React:
市佔最大的前端框架
用 JS 寫一個 button 沒問題,但寫一個有 100 個 button、互相要溝通的後台呢? Meta 工程師當年也崩潰,所以做了 React。它把「畫面 = 函式(資料)」變成口頭禪,整個前端產業跟著轉向。 現在 70% 的前端職缺要 React,會它就有飯吃。
- 看懂並能改任何 React 專案
- 會用 component、props、state、hooks
- 會發 API、處理 loading / error
- 會用 Context 跨元件分享資料
- 準備好寫 Next.js 全端應用
LESSON 6.1
為什麼要用 React?
看這個純 JS:
let count = 0;
btn.addEventListener('click', () => {
count++;
display.textContent = count;
badge.textContent = count;
title.textContent = `你已點 ${count} 次`;
// 5 個地方要手動同步...
});
每個用到 count 的地方都要記得改。漏一個就 bug。
React 把它變成:
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>+</button>
<p>{count}</p>
<span>{count}</span>
<h1>你已點 {count} 次</h1>
</>
);
}
你只管「資料」,畫面自動跟。這就是 React 的整個哲學。
LESSON 6.2
怎麼開始一個 React 專案
# 用 Vite(最快、最現代)
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
瀏覽器打開 http://localhost:5173 就看到了。
專案結構
my-app/
├── src/
│ ├── App.tsx ← 主元件
│ ├── main.tsx ← 進入點
│ └── components/ ← 你的元件
├── public/ ← 靜態資源
├── index.html
└── package.json
LESSON 6.3
JSX:JS 裡寫 HTML
function Hello() {
const name = '小明';
return (
<div className="greeting">
<h1>哈囉 {name}!</h1>
<p>今天 {new Date().toLocaleDateString()}</p>
</div>
);
}
JSX 跟 HTML 差在哪
class→className(class 是 JS 關鍵字)for→htmlFor- 屬性用 camelCase:
onClick、onChange、tabIndex - style 是物件:
style={{ color: 'red', fontSize: 16 }} - 單身標籤要自閉合:
<img />、<input /> - 必須只有一個 root 元素,多個用
<>...</>
條件與迴圈
{isLoggedIn && <p>歡迎回來</p>}
{user ? <Profile /> : <Login />}
<ul>
{fruits.map(f => (
<li key={f.id}>{f.name}</li>
))}
</ul>
key 一定要寫,且要穩定唯一。不要用 array index(會讓動畫錯亂)。用資料的 id。
LESSON 6.4
元件與 Props
元件 = 可重用的 JS 函式,回傳 JSX。
type CardProps = {
title: string;
desc: string;
onClick?: () => void;
};
function Card({ title, desc, onClick }: CardProps) {
return (
<div className="card" onClick={onClick}>
<h3>{title}</h3>
<p>{desc}</p>
</div>
);
}
// 使用
<Card title="標題" desc="描述" onClick={() => alert('hi')} />
children
function Box({ children }: { children: React.ReactNode }) {
return <div className="box">{children}</div>;
}
<Box>
<h1>隨便放東西</h1>
<p>都會放進 box</p>
</Box>
LESSON 6.5
useState:管理狀態
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
return (
<>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
</>
);
}
更新陣列、物件
const [todos, setTodos] = useState([]);
// 新增
setTodos([...todos, newTodo]);
// 刪除
setTodos(todos.filter(t => t.id !== id));
// 更新某項
setTodos(todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
不要 todos.push(x) 然後 setTodos(todos)。React 看引用判斷有沒有變,原陣列引用沒變不會更新。永遠建立新陣列。
LESSON 6.6
useEffect:副作用
進元件後要做的事:發 API、訂閱、定時器。
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(d => { setUser(d); setLoading(false); });
}, [userId]); // userId 變才重跑
if (loading) return <p>載入中...</p>;
return <h1>{user.name}</h1>;
}
依賴陣列三種寫法
[]= 只執行一次(mount 時)[x]= x 變才執行- 不寫 = 每次 render 都執行(通常是 bug)
清理
useEffect(() => {
const id = setInterval(() => tick(), 1000);
return () => clearInterval(id); // 元件卸載時清理
}, []);
LESSON 6.7
其他常用 Hook
// useRef:抓 DOM 或存「不觸發 re-render 的值」
const inputRef = useRef(null);
<input ref={inputRef} />
inputRef.current.focus();
// useMemo:快取昂貴計算
const sorted = useMemo(() => items.sort(...), [items]);
// useCallback:快取函式(傳給子元件用)
const handleClick = useCallback(() => doX(id), [id]);
// useContext:跨層分享資料
const ThemeCtx = createContext('light');
<ThemeCtx.Provider value="dark">...</ThemeCtx.Provider>
const theme = useContext(ThemeCtx);
不要過早優化。useMemo 和 useCallback 不是「加了會更快」。沒效能問題的元件加它們反而是負擔。
LESSON 6.8
自訂 Hook
// 抽出可重用邏輯
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// 用
const [theme, setTheme] = useLocalStorage('theme', 'light');
LESSON 6.9
狀態管理選擇
| 專案規模 | 推薦 |
|---|---|
| 個人 / 小專案 | useState + useContext |
| 中型 | Zustand(5KB、超簡單) |
| 大型 | Redux Toolkit、Jotai |
| API 資料 | TanStack Query(必裝) |
// Zustand 範例(10 行學會)
import { create } from 'zustand';
const useStore = create(set => ({
count: 0,
inc: () => set(s => ({ count: s.count + 1 }))
}));
// 任何元件都可以用
const { count, inc } = useStore();
練習:React 版 To-Do List
把第 3 章的 To-Do List 改寫成 React + TS:
- 建立
useLocalStorage自訂 hook - 拆三個元件:
<TodoForm />、<TodoList />、<TodoItem /> - 定義
Todotype - filter 切換:全部 / 未完成 / 已完成
- 計數器:「還有 X 項未完成」
- 加入動畫:新增 / 刪除有過渡
常見卡關 FAQ
// React 學員最常問的問題
為什麼 setState 後 console.log 印出來還是舊值?
setState 是非同步的。下一次 render 才會是新值。要拿新值請放 useEffect 裡聽 state 變化。useEffect 跑了兩次!
開發模式 React 18+ 的 StrictMode 會故意跑兩次,幫你抓「沒清理副作用」的 bug。Production 不會。請寫好 cleanup function。
點擊事件後畫面沒更新?
99% 是直接修改了原物件 / 陣列。要建新的:
setX([...x, item])、setX({ ...x, key: val })。props drilling 太深怎麼辦?
三層以下用 props 沒關係。四層以上用 Context 或 Zustand。但別什麼都塞 Context,會讓所有訂閱元件一起 re-render。
React 還是 Vue 該學哪個?
求職以 React 機會多。學習曲線 Vue 較緩。中文社群兩邊都活躍。建議:學一個學透,另一個一週就能上手。學會 React 後第 7 章的 Vue 你會覺得簡單。