Skip to content
第 149 / 250 章前端⏱ 10 分钟阅读

第 149 章:Hooks 之 useEffect

学习目标

  • 理解副作用(Side Effect)概念
  • 掌握 useEffect 的三种触发时机
  • 学会清理函数
  • 避免 useEffect 常见反模式

一、什么是副作用

渲染之外的操作就是副作用:

二、useEffect 基础

tsx
'use client';
import { useEffect, useState } from 'react';

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);

  // ① 基础形式
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setUser);
  }, [userId]);  // ② 依赖数组

  return <div>{user?.name}</div>;
}

三、三种触发时机

3.1 每次渲染都执行(不推荐)

tsx
useEffect(() => {
  console.log('每次都执行');
});  // 没有依赖数组

3.2 只在挂载时执行一次

tsx
useEffect(() => {
  console.log('挂载时执行一次');
  fetchInitialData();
}, []);   // 空数组

3.3 依赖变化时执行

tsx
useEffect(() => {
  console.log('userId 变化了');
  fetchUser(userId);
}, [userId]);  // 依赖 userId

四、清理函数

tsx
useEffect(() => {
  const timer = setInterval(() => {
    console.log('tick');
  }, 1000);

  // 清理:组件卸载或下次 effect 前执行
  return () => {
    clearInterval(timer);
  };
}, []);

4.1 订阅清理

tsx
useEffect(() => {
  const sub = ws.subscribe('msg', handleMsg);
  return () => sub.unsubscribe();
}, []);

4.2 取消请求

tsx
useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/users/${userId}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setUser)
    .catch(e => {
      if (e.name !== 'AbortError') console.error(e);
    });

  return () => controller.abort();
}, [userId]);

五、常见场景

5.1 数据获取

tsx
function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch('/api/users')
      .then(r => r.json())
      .then(setUsers)
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <Spinner />;
  return <List users={users} />;
}

5.2 订阅外部 store

tsx
function useWindowSize() {
  const [size, setSize] = useState({ w: 0, h: 0 });

  useEffect(() => {
    function update() {
      setSize({ w: window.innerWidth, h: window.innerHeight });
    }
    update();
    window.addEventListener('resize', update);
    return () => window.removeEventListener('resize', update);
  }, []);

  return size;
}

5.3 同步到 localStorage

tsx
function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initial;
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initial;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;
}

六、依赖数组的坑

6.1 必须列出所有依赖

tsx
// ❌ 错:漏掉 userId
useEffect(() => {
  fetchUser(userId);
}, []);

// ✅ 对
useEffect(() => {
  fetchUser(userId);
}, [userId]);

6.2 函数依赖

tsx
// ❌ 错:每次渲染 fetchUser 都是新函数
useEffect(() => {
  fetchUser(userId);
}, [fetchUser]);

// ✅ 用 useCallback 稳定
const fetchUser = useCallback((id: number) => {
  // ...
}, []);

useEffect(() => {
  fetchUser(userId);
}, [userId, fetchUser]);

七、不要在 useEffect 里做的事

7.1 不要直接处理 props/state 转换

tsx
// ❌ 反模式
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// ✅ 用 useMemo 或直接计算
const fullName = `${firstName} ${lastName}`;

7.2 不要处理用户事件

tsx
// ❌ 反模式
useEffect(() => {
  if (submitted) sendData();
}, [submitted]);

// ✅ 直接在事件处理
function handleSubmit() {
  sendData();
  setSubmitted(true);
}

八、严格模式双调用

开发模式下,React 会故意双调用 effect,用于发现清理问题。

tsx
useEffect(() => {
  const sub = api.subscribe();
  return () => sub.unsubscribe();  // 必须有清理
}, []);

九、本章小结

概念关键
副作用渲染之外的操作
useEffectuseEffect(fn, deps)
依赖列出所有用到的外部值
清理返回 () => { /* cleanup */ }
一次执行空依赖 []
严格模式开发模式 effect 会双调用

动手练习

  1. 用 useEffect 获取用户列表并展示
  2. 写一个 useWindowSize hook
  3. 写一个 useLocalStorage hook
  4. 实现一个倒计时组件(setInterval + 清理)
  5. 实现 useTitle hook(修改 document.title)

推荐阅读


下一章:第 150 章:Hooks 之 useRef / useMemo / useCallback

本站基于 VitePress 构建 · 由 Codebook 团队维护