cover

概念圖

什麼是 Hooks?

Hooks 是 React 16.8 引入的 API,讓你在函式元件中使用狀態和其他 React 特性,不需要寫 Class 元件。

// ❌ 以前:Class 元件
class Counter extends React.Component {
  state = { count: 0 };
 
  render() {
    return (
      <button onClick={() => this.setState({ count: this.state.count + 1 })}>
        {this.state.count}
      </button>
    );
  }
}
 
// ✅ 現在:函式元件 + Hooks
function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

useState - 狀態管理

import { useState } from 'react';
 
function Example() {
  // 宣告一個狀態變數 count,初始值為 0
  const [count, setCount] = useState(0);
 
  // 可以有多個 state
  const [name, setName] = useState('');
  const [items, setItems] = useState([]);
  const [user, setUser] = useState({ name: '', age: 0 });
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(prev => prev + 1)}>+1 (函式更新)</button>
    </div>
  );
}

更新物件/陣列狀態

// 更新物件 - 需要展開原有屬性
const [user, setUser] = useState({ name: '', age: 0 });
setUser({ ...user, name: 'John' });
setUser(prev => ({ ...prev, age: prev.age + 1 }));
 
// 更新陣列
const [items, setItems] = useState([]);
setItems([...items, newItem]);           // 新增
setItems(items.filter(item => item.id !== id));  // 刪除
setItems(items.map(item =>
  item.id === id ? { ...item, done: true } : item
));  // 更新特定項目

useEffect - 副作用處理

處理「副作用」:資料請求、訂閱、DOM 操作等。

import { useState, useEffect } from 'react';
 
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
 
  // 基本用法:每次渲染後執行
  useEffect(() => {
    console.log('元件渲染了');
  });
 
  // 加上依賴陣列:只在 userId 變化時執行
  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);
 
  // 空陣列:只在掛載時執行一次
  useEffect(() => {
    console.log('元件掛載了');
  }, []);
 
  // 清理函式:元件卸載或依賴變化前執行
  useEffect(() => {
    const subscription = api.subscribe(userId);
 
    return () => {
      subscription.unsubscribe();  // 清理
    };
  }, [userId]);
 
  if (loading) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}

useEffect 執行時機

┌─────────────────────────────────────────────────┐
│  useEffect(() => { ... })                       │
│  → 每次渲染後執行                                │
├─────────────────────────────────────────────────┤
│  useEffect(() => { ... }, [])                   │
│  → 只在掛載時執行一次                            │
├─────────────────────────────────────────────────┤
│  useEffect(() => { ... }, [dep1, dep2])         │
│  → 掛載時 + dep1 或 dep2 變化時執行              │
├─────────────────────────────────────────────────┤
│  useEffect(() => {                              │
│    return () => { /* cleanup */ }               │
│  }, [dep])                                      │
│  → dep 變化前 或 卸載時 執行 cleanup             │
└─────────────────────────────────────────────────┘

useContext - 跨元件共享狀態

import { createContext, useContext, useState } from 'react';
 
// 1. 建立 Context
const ThemeContext = createContext();
 
// 2. 建立 Provider
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
 
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
 
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
 
// 3. 使用 Context
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
 
  return (
    <button
      onClick={toggleTheme}
      style={{ background: theme === 'light' ? '#fff' : '#333' }}
    >
      切換主題
    </button>
  );
}
 
// 4. 在 App 中使用
function App() {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  );
}

useRef - 參照和持久值

import { useRef, useEffect } from 'react';
 
function TextInput() {
  // 1. 參照 DOM 元素
  const inputRef = useRef(null);
 
  useEffect(() => {
    inputRef.current.focus();  // 自動聚焦
  }, []);
 
  return <input ref={inputRef} />;
}
 
function Timer() {
  // 2. 儲存不需要觸發重新渲染的值
  const intervalRef = useRef(null);
  const countRef = useRef(0);  // 不會觸發重新渲染
 
  const startTimer = () => {
    intervalRef.current = setInterval(() => {
      countRef.current += 1;
      console.log(countRef.current);
    }, 1000);
  };
 
  const stopTimer = () => {
    clearInterval(intervalRef.current);
  };
 
  return (
    <>
      <button onClick={startTimer}>開始</button>
      <button onClick={stopTimer}>停止</button>
    </>
  );
}

useMemo - 快取計算結果

避免昂貴的計算在每次渲染時重複執行。

import { useMemo, useState } from 'react';
 
function ExpensiveComponent({ items, filter }) {
  // ❌ 每次渲染都會重新計算
  const filteredItems = items.filter(item => item.includes(filter));
 
  // ✅ 只在 items 或 filter 變化時才重新計算
  const filteredItems = useMemo(() => {
    console.log('計算中...');
    return items.filter(item => item.includes(filter));
  }, [items, filter]);
 
  return (
    <ul>
      {filteredItems.map(item => <li key={item}>{item}</li>)}
    </ul>
  );
}

useCallback - 快取函式

避免子元件因為函式引用改變而不必要地重新渲染。

import { useCallback, useState, memo } from 'react';
 
// 用 memo 包裝的子元件只在 props 改變時重新渲染
const Button = memo(({ onClick, children }) => {
  console.log('Button 渲染');
  return <button onClick={onClick}>{children}</button>;
});
 
function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
 
  // ❌ 每次渲染都建立新函式,導致 Button 重新渲染
  const handleClick = () => setCount(c => c + 1);
 
  // ✅ 快取函式,Button 不會因為 name 改變而重新渲染
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);
 
  return (
    <>
      <input value={name} onChange={e => setName(e.target.value)} />
      <Button onClick={handleClick}>Count: {count}</Button>
    </>
  );
}

自訂 Hook

將邏輯封裝成可重用的 Hook。

// useLocalStorage - 同步 localStorage
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : initialValue;
  });
 
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
 
  return [value, setValue];
}
 
// 使用
function App() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  // ...
}
// useFetch - 資料請求
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);
 
  return { data, loading, error };
}
 
// 使用
function UserList() {
  const { data: users, loading, error } = useFetch('/api/users');
 
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Hooks 規則

  1. 只在最頂層呼叫 Hooks - 不要在迴圈、條件式或巢狀函式中呼叫
  2. 只在 React 函式中呼叫 Hooks - 不要在一般 JavaScript 函式中呼叫
// ❌ 錯誤
if (condition) {
  const [value, setValue] = useState(0);
}
 
// ✅ 正確
const [value, setValue] = useState(0);
if (condition) {
  // 使用 value
}

延伸閱讀

React 官方文件 - Hooks 前端使用哪些方法跟API互動 前端mvp設計原則