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 差在哪

條件與迴圈

{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>;
}

依賴陣列三種寫法

清理

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);

不要過早優化。useMemouseCallback 不是「加了會更快」。沒效能問題的元件加它們反而是負擔。

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:

  1. 建立 useLocalStorage 自訂 hook
  2. 拆三個元件:<TodoForm /><TodoList /><TodoItem />
  3. 定義 Todo type
  4. filter 切換:全部 / 未完成 / 已完成
  5. 計數器:「還有 X 項未完成」
  6. 加入動畫:新增 / 刪除有過渡

常見卡關 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 你會覺得簡單。
← 上一章
06 UX