☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
react 中級

🪝 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

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊