🪝 React Hooks 深入:useEffect、useRef、useContext
📌 useEffect — 副作用管理
useEffect 讓你在元件渲染後執行「副作用」操作,例如 API 呼叫、設定定時器、訂閱事件等。
⚠️ 什麼是副作用? 任何不是「根據 props/state 算出 UI」的行為都是副作用。 原生 JS 直接在全域寫就好,但 React 需要
useEffect來確保時機正確。
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// useEffect(副作用函式, 依賴陣列)
useEffect(() => {
setLoading(true);
// 呼叫 API(副作用)
fetch(`https://api.example.com/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
// 清理函式(元件卸載或依賴改變時執行)
return () => {
console.log('清理上一次的副作用');
};
}, [userId]); // 只在 userId 改變時重新執行
if (loading) return <p>載入中...</p>;
return <h1>{user?.name}</h1>;
}
useEffect 依賴陣列規則
// 1. 無依賴陣列 → 每次渲染都執行(通常不建議)
useEffect(() => { /* 每次渲染後都執行 */ });
// 2. 空陣列 → 只在掛載時執行一次(相當於 componentDidMount)
useEffect(() => { /* 只執行一次 */ }, []);
// 3. 有依賴 → 依賴改變時才執行
useEffect(() => { /* userId 或 token 改變時執行 */ }, [userId, token]);
🔗 useRef — DOM 參照與持久值
useRef 有兩個用途:取得 DOM 元素參照 和 儲存不觸發重新渲染的值。
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef(null); // 建立 ref
useEffect(() => {
inputRef.current.focus(); // 直接操作 DOM(像原生 JS)
}, []);
return <input ref={inputRef} placeholder="自動聚焦" />;
}
useRef 儲存不重新渲染的值
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null); // 不會觸發重新渲染
const start = () => {
intervalRef.current = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current); // 用 ref 存定時器 ID
};
return (
<div>
<p>{seconds} 秒</p>
<button onClick={start}>開始</button>
<button onClick={stop}>停止</button>
</div>
);
}
💡 useState vs useRef:
useState:值改變 → 觸發重新渲染useRef:值改變 → 不觸發重新渲染(適合存定時器 ID、前一次值等)
🌐 useContext — 跨元件狀態共享
不用層層傳 props,直接跨元件共享資料。
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. 任何子元件都可以用 useContext 取得
function Header() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<header style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
<h1>目前主題:{theme}</h1>
<button onClick={toggleTheme}>切換主題</button>
</header>
);
}
// 4. 使用
function App() {
return (
<ThemeProvider>
<Header /> {/* Header 不需要接收 props */}
<MainContent />
</ThemeProvider>
);
}
⚡ useMemo 和 useCallback — 效能優化
import { useMemo, useCallback, useState } from 'react';
function ExpensiveComponent({ items, onItemClick }) {
// useMemo:快取計算結果,依賴不變就不重算
const sortedItems = useMemo(() => {
console.log('排序中...'); // 只在 items 改變時執行
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
return (
<ul>
{sortedItems.map(item => (
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
function App() {
const [items] = useState([{ id: 1, name: 'Banana' }, { id: 2, name: 'Apple' }]);
// useCallback:快取函式參照,避免子元件不必要的重新渲染
const handleClick = useCallback((id) => {
console.log('點擊了項目', id);
}, []); // 空依賴 → 函式不會改變
return <ExpensiveComponent items={items} onItemClick={handleClick} />;
}
🧩 自訂 Hook(Custom Hooks)
把重複邏輯抽成可重用的 Hook,名稱必須以 use 開頭。
// useLocalStorage.js — 自訂 Hook
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]; // 和 useState 一樣的介面
}
// 使用自訂 Hook
function Settings() {
const [darkMode, setDarkMode] = useLocalStorage('darkMode', false);
return (
<label>
<input
type="checkbox"
checked={darkMode}
onChange={(e) => setDarkMode(e.target.checked)}
/>
深色模式
</label>
);
}
⚠️ 常見陷阱
閉包陷阱(Stale Closure)
function StaleClosureDemo() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// ❌ 錯誤:count 永遠是 0(閉包捕獲了初始值)
// setCount(count + 1);
// ✅ 正確:用函式型更新,取得最新值
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // 空依賴 → effect 只建立一次
return <p>{count}</p>;
}
無限迴圈
// ❌ 錯誤:每次渲染都建立新物件 → 觸發 useEffect → 又設定 state → 無限迴圈
useEffect(() => {
setData({ ...data, loaded: true });
}, [data]); // data 每次都是新物件!
// ✅ 正確:精確指定依賴,或使用函式型更新
useEffect(() => {
setData(prev => ({ ...prev, loaded: true }));
}, []); // 只執行一次
✅ 本章重點
| Hook | 用途 | 常見場景 |
|---|---|---|
| useEffect | 副作用管理 | API 呼叫、定時器、訂閱 |
| useRef | DOM 參照 / 持久值 | 聚焦 input、存定時器 ID |
| useContext | 跨元件共享狀態 | 主題、語言、使用者資訊 |
| useMemo | 快取計算結果 | 大量資料排序 / 過濾 |
| useCallback | 快取函式參照 | 傳給子元件的回呼 |
| 自訂 Hook | 重用邏輯 | useLocalStorage、useFetch |