首页 / 文章 / 不要一遇到 React 相关问题就默认使用 useEffect。

不要一遇到 React 相关问题就默认使用 useEffect。

效果与外部系统同步;在渲染时生成,通过事件处理点击操作,利用真实数据工具进行获取。

1424 词

useEffect 的陷阱

学习 Hooks 的人常常会用 useEffect 来处理数据获取、派生状态以及按钮点击事件。这些效应用于与外部系统同步,而非处理常规渲染逻辑的默认工具。

useEffect 实际上做什么

在组件渲染完成后,React 会执行这些效应以添加监听器、建立连接或以命令式方式与 React 外部的对象交互。不当使用会导致界面闪烁、重复获取数据以及竞态条件错误。

// ❌ BAD: Two renders, unnecessary state
function UserProfile({ firstName, lastName }) {
  const [fullName, setFullName] = useState('');
useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
  }, [firstName, lastName]);
  return <h1>{fullName}</h1>;
}
// ✅ GOOD: Calculate during render
function UserProfile({ firstName, lastName }) {
  const fullName = `${firstName} ${lastName}`;
return <h1>{fullName}</h1>;
}
// ❌ BAD: Unnecessary effect + state
function ProductList({ products }) {
  const [sortedProducts, setSortedProducts] = useState([]);
useEffect(() => {
    const sorted = [...products].sort((a, b) => a.price - b.price);
    setSortedProducts(sorted);
  }, [products]);
  return (
    <ul>
      {sortedProducts.map(product => (
        <li key={product.id}>{product.name} - ${product.price}</li>
      ))}
    </ul>
  );
}
// ✅ GOOD: Calculate during render (useMemo for expensive ops)
function ProductList({ products }) {
  const sortedProducts = useMemo(() => {
    return [...products].sort((a, b) => a.price - b.price);
  }, [products]);
return (
    <ul>
      {sortedProducts.map(product => (
        <li key={product.id}>{product.name} - ${product.price}</li>
      ))}
    </ul>
  );
}
// ❌ BAD: Manual fetching with useEffect
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
useEffect(() => {
    let cancelled = false;
    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();
        if (!cancelled) setUser(data);
      } catch (err) {
        if (!cancelled) setError(err.message);
      } finally {
        if (!cancelled) setLoading(false);
      }
    }
    fetchUser();
    return () => { cancelled = true; };
  }, [userId]);
  if (loading) return <Spinner />;
  if (error) return <p>Error: {error}</p>;
  return <div>{user.name}</div>;
}
// ✅ GOOD: Use TanStack Query (React Query)
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
  const { data: user, isPending, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  });
  if (isPending) return <Spinner />;
  if (error) return <p>Error loading user</p>;
  return <div>{user.name}</div>;
}
// ❌ BAD: Overcomplicating with useEffect
function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);
useEffect(() => {
    if (liked) {
      fetch(`/api/posts/${postId}/like`, { method: 'POST' });
    } else {
      fetch(`/api/posts/${postId}/unlike`, { method: 'POST' });
    }
  }, [liked, postId]);
  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️' : '🤍'}
    </button>
  );
}
// ✅ GOOD: Handle in the event handler
function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);
const handleLike = async () => {
    const newLiked = !liked;
    setLiked(newLiked);
    await fetch(`/api/posts/${postId}/${newLiked ? 'like' : 'unlike'}`, {
      method: 'POST',
    });
  };
  return (
    <button onClick={handleLike}>
      {liked ? '❤️' : '🤍'}
    </button>
  );
}
function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);

    // Cleanup is crucial!
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  return <p>Window width: {width}px</p>;
}
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
useEffect(() => {
    const connection = createWebSocketConnection(roomId);

    connection.onMessage = (msg) => {
      setMessages(prev => [...prev, msg]);
    };
    connection.connect();
    return () => {
      connection.disconnect(); // Cleanup!
    };
  }, [roomId]);
  return <MessageList messages={messages} />;
}
function ChartComponent({ data }) {
  const chartRef = useRef(null);
  const chartInstance = useRef(null);
useEffect(() => {
    if (chartRef.current && !chartInstance.current) {
      chartInstance.current = new ChartLibrary(chartRef.current, {
        type: 'line',
        data: data,
      });
    }
    return () => {
      chartInstance.current?.destroy();
      chartInstance.current = null;
    };
  }, []);
  return <canvas ref={chartRef} />;
}
function PageTitle({ title }) {
  useEffect(() => {
    document.title = `${title} | My App`;

    return () => {
      document.title = 'My App'; // Reset on unmount
    };
  }, [title]);
return null;
}

四种常见错误

1. 在效应中计算值

在渲染过程中进行计算(或当计算成本较高时使用 useMemo)。那些通过 props/state 调用 setState 的效应往往会导致不被允许的反馈循环。

2. 效应中的高成本计算

应将纯粹的计算逻辑放在渲染/记忆化阶段,而非绘制之后。

3. 无约束的数据获取效应

数据获取需要具备取消功能、缓存机制以及错误处理界面。建议使用专门的库或框架来管理加载过程;若必须使用效应,则需妥善处理中断和竞态条件。

4. 效应中的用户操作处理

点击处理逻辑应放在事件处理函数中。因点击操作导致标志位变化而触发的效应会增加间接性并引发错误。

决策树

如果是基于现有的状态/props 进行计算 → 使用渲染。如果是响应点击/提交操作 → 使用事件处理函数。如果需要与 DOM API、网络数据或第三方组件的生命周期交互 → 使用效应。

何时适合使用效应

外部事件监听器

WebSockets

第三方库初始化

需要正确释放资源的非React命令式API

总结

减少副作用,让数据流更清晰。将useEffect用于与React之外的系统同步,而非作为每次渲染后处理任务的默认手段。

严格模式会在开发环境中重复触发副作用,以便发现未完成的清理工作——在处理Socket连接时可将此视为一种优势。对于服务器状态,建议使用声明式数据库,让组件描述依赖关系而非手动实现获取数据的逻辑。在添加仅用于将属性复制到状态的副作用之前,请先进行评估;这种模式通常是对方向性理解的误解。

清理函数必须移除监听器和定时器;残留的处理程序是导致“组件已卸载仍调用setState”警告的常见原因。

依赖数组是契约的一部分——若遗漏值,则会出现过时的闭包;如果列出的对象和效应过多,每次渲染都会导致性能下降。

在与URL搜索参数或外部存储同步时,建议使用专门设计的钩子来封装效应的边界。

服务端渲染的应用程序不应在缺乏保护机制的情况下启动仅适用于浏览器的效应,而应在环境或生命周期阶段进行控制。

清理函数必须移除监听器和定时器;残留的处理器是导致“组件已卸载仍尝试setState”警告的常见原因。

依赖数组是契约的一部分——若遗漏值,则会出现过时的闭包;如果列出的对象和效应过多,每次渲染都会导致性能下降。

在与URL搜索参数或外部存储同步时,建议使用专门设计的钩子来封装效应的边界。

服务器端渲染的应用程序不应在缺乏防护措施的情况下启动仅适用于浏览器的效果;应在环境或生命周期阶段进行控制。

清理函数必须移除监听器和定时器;残留的处理程序是导致“组件已卸载仍尝试调用setState”警告的常见原因。

依赖数组是约定的一部分——若遗漏相关值,就会出现过时的闭包;过度列出对象和效果会导致每次渲染时性能下降。

在与URL搜索参数或外部存储同步数据时,应优先使用专门设计的钩子函数来封装相关逻辑。

服务器端渲染的应用程序不应在缺乏防护措施的情况下启动仅适用于浏览器的效果;应在环境或生命周期阶段进行控制。

清理函数必须移除监听器和定时器;残留的处理程序是导致“组件已卸载仍尝试调用setState”警告的常见原因。

依赖数组是契约的一部分——若遗漏值,则会出现过时的闭包;如果列出的对象和效应过多,每次渲染都会导致性能下降。

在与URL搜索参数或外部存储同步时,建议使用专门设计的钩子来封装效应的边界。

服务端渲染的应用程序不应在缺乏保护机制的情况下启动仅适用于浏览器的效应,而应在环境或生命周期阶段进行控制。

清理函数必须移除监听器和定时器;残留的处理程序是导致“组件已卸载仍尝试setState”警告的常见原因。

依赖数组是契约的一部分——若遗漏值,则会出现过时的闭包;如果列出的对象和效应过多,每次渲染都会导致性能下降。

在与URL搜索参数或外部存储同步时,建议使用专门设计的钩子来封装效应的边界。

服务器端渲染的应用程序不应在缺乏防护措施的情况下启动仅适用于浏览器的效果;应在环境或生命周期阶段进行控制。

清理函数必须移除监听器和定时器;残留的处理程序是导致“组件已卸载仍尝试调用setState”警告的常见原因。

依赖数组是约定的一部分——若遗漏相关值,就会出现过时的闭包;过度列出对象和效果会导致每次渲染时性能下降。

在与URL搜索参数或外部存储同步数据时,应优先使用专门设计的钩子函数来封装相关逻辑。

服务器端渲染的应用程序不应在缺乏防护措施的情况下启动仅适用于浏览器的效果;应在环境或生命周期阶段进行控制。

清理函数必须移除监听器和定时器;残留的处理程序是导致“组件已卸载仍尝试调用setState”警告的常见原因。

依赖数组是契约的一部分——若遗漏值或使用过期的闭包,就会出现问题;如果列出的对象和效应过多,每次渲染都会变得极其缓慢。

在与 URL 查询参数或外部存储同步时,建议使用专为该目的设计的钩子来封装效应的边界。

服务端渲染的应用不应在缺乏保护机制的情况下启动仅适用于浏览器的效应;应在环境或生命周期阶段进行控制。

清理函数必须移除监听器和定时器;残留的处理程序是导致“组件已卸载仍尝试调用 setState”警告的常见原因。