React useEffect 深度解析:副作用、争议与现代替代方案

什么是副作用(Side Effects)?

在 React 中,副作用指的是那些不直接参与渲染输出的操作。理解副作用是掌握 useEffect 的第一步。

纯函数 vs 副作用

TSX
// ✅ 纯函数:相同输入 → 相同输出,无副作用
function calculateTotal(price: number, quantity: number) {
  return price * quantity;
}

// ❌ 有副作用:修改外部状态、发起网络请求
function saveToDatabase(data: any) {
  fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
  localStorage.setItem('cache', JSON.stringify(data));
  console.log('Saved:', data);
}

React 中的常见副作用

渲染中...

useEffect 的基本用法

语法结构

TSX
useEffect(() => {
  // 副作用逻辑

  return () => {
    // 清理函数(可选)
  };
}, [依赖项数组]);

三种依赖模式

useEffect 的争议点

1. 执行时机难以理解

TSX
function Counter() {
  const [count, setCount] = useState(0);

  // ❌ 新手常见误区:以为会立即执行
  useEffect(() => {
    console.log('当前 count:', count);
  }, [count]);

  console.log('渲染时 count:', count);

  return <button onClick={() => setCount(count + 1)}>点击</button>;
}

执行顺序

渲染中...

2. 依赖项地狱

TSX
// ❌ 依赖项越来越多,难以维护
useEffect(() => {
  if (userId && isLoggedIn && !isLoading && hasPermission) {
    fetchData(userId, filters, sortBy, page, pageSize);
  }
}, [userId, isLoggedIn, isLoading, hasPermission, filters, sortBy, page, pageSize]);

3. 竞态条件(Race Condition)

TSX
// ❌ 快速切换用户时,可能显示错误的数据
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(data => {
      setUser(data); // 可能是旧请求的数据!
    });
  }, [userId]);

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

问题演示

渲染中...

正确做法

TSX
// ✅ 使用清理函数处理竞态
useEffect(() => {
  let cancelled = false;

  fetchUser(userId).then(data => {
    if (!cancelled) {
      setUser(data);
    }
  });

  return () => {
    cancelled = true;
  };
}, [userId]);

现代替代方案

1. React 19 的 use Hook

TSX
import { use } from 'react';

// ✅ 更简洁的数据获取
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);

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

// 使用方式
function App() {
  const userPromise = fetchUser('123');

  return (
    <Suspense fallback={<Loading />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

优势对比

2. TanStack Query(React Query)

TSX
import { useQuery } from '@tanstack/react-query';

// ✅ 自动处理缓存、重试、竞态
function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000, // 5分钟内使用缓存
  });

  if (isLoading) return <Loading />;
  if (error) return <Error error={error} />;
  return <div>{user.name}</div>;
}

TanStack Query 的优势

渲染中...

3. 事件处理器(而非 useEffect)

TSX
// ❌ 不要用 useEffect 处理用户交互
function SearchBox() {
  const [query, setQuery] = useState('');

  useEffect(() => {
    if (query) {
      logSearch(query);
    }
  }, [query]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

// ✅ 直接在事件处理器中处理
function SearchBox() {
  const [query, setQuery] = useState('');

  const handleSearch = (value: string) => {
    setQuery(value);
    if (value) {
      logSearch(value);
    }
  };

  return <input value={query} onChange={e => handleSearch(e.target.value)} />;
}

4. 派生状态(而非 useEffect 同步)

TSX
// ❌ 不要用 useEffect 同步状态
function TodoList({ todos }: { todos: Todo[] }) {
  const [completedCount, setCompletedCount] = useState(0);

  useEffect(() => {
    setCompletedCount(todos.filter(t => t.completed).length);
  }, [todos]);

  return <div>已完成: {completedCount}</div>;
}

// ✅ 直接计算派生状态
function TodoList({ todos }: { todos: Todo[] }) {
  const completedCount = todos.filter(t => t.completed).length;

  return <div>已完成: {completedCount}</div>;
}

// ✅ 如果计算昂贵,使用 useMemo
function TodoList({ todos }: { todos: Todo[] }) {
  const completedCount = useMemo(
    () => todos.filter(t => t.completed).length,
    [todos]
  );

  return <div>已完成: {completedCount}</div>;
}

useEffect 的合理使用场景

虽然有很多替代方案,但 useEffect 在某些场景下仍然是最佳选择:

1. 与外部系统同步

TSX
// ✅ WebSocket 连接
function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<Message[]>([]);

  useEffect(() => {
    const ws = new WebSocket(`wss://chat.example.com/${roomId}`);

    ws.onmessage = (event) => {
      setMessages(prev => [...prev, JSON.parse(event.data)]);
    };

    return () => {
      ws.close();
    };
  }, [roomId]);

  return <MessageList messages={messages} />;
}

2. 浏览器 API 集成

TSX
// ✅ 监听窗口大小变化
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

3. 第三方库初始化

TSX
// ✅ 初始化图表库
function Chart({ data }: { data: number[] }) {
  const chartRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!chartRef.current) return;

    const chart = new ChartLibrary(chartRef.current, {
      data,
      type: 'line',
    });

    return () => {
      chart.destroy();
    };
  }, [data]);

  return <div ref={chartRef} />;
}

4. 分析和日志

TSX
// ✅ 页面浏览统计
function ProductPage({ productId }: { productId: string }) {
  useEffect(() => {
    analytics.track('page_view', {
      page: 'product',
      productId,
      timestamp: Date.now(),
    });
  }, [productId]);

  return <ProductDetails productId={productId} />;
}

决策流程图

渲染中...

最佳实践总结

✅ 推荐做法

  1. 优先考虑替代方案:事件处理器 > 派生状态 > TanStack Query > use Hook > useEffect
  2. 保持 useEffect 简单:一个 useEffect 只做一件事
  3. 正确处理清理:订阅、定时器、连接都要清理
  4. 避免依赖项遗漏:使用 ESLint 规则 react-hooks/exhaustive-deps

❌ 避免的做法

  1. 不要用 useEffect 同步状态:使用派生状态
  2. 不要用 useEffect 处理用户交互:使用事件处理器
  3. 不要忽略竞态条件:使用清理函数或 TanStack Query
  4. 不要过度使用:很多场景不需要 useEffect

总结

useEffect 是 React 中强大但容易误用的工具。随着 React 19 和现代状态管理库的发展,很多传统的 useEffect 使用场景都有了更好的替代方案:

  • 数据获取 → TanStack Query 或 use Hook
  • 状态同步 → 派生状态或 useMemo
  • 用户交互 → 事件处理器
  • 外部系统 → 仍然使用 useEffect(合理场景)

理解副作用的本质,选择合适的工具,才能写出更清晰、更可维护的 React 代码。