useEffect 闭包陷阱

理解 React useEffect 中的闭包问题及解决方案

问题

在 React 中使用 useEffect 时,回调函数中访问的 state 或 props 可能是过时的值,这就是闭包陷阱。

解答

问题复现

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // 定时器回调捕获了初始的 count 值 (0)
    const timer = setInterval(() => {
      console.log('当前 count:', count); // 永远输出 0
    }, 1000);

    return () => clearInterval(timer);
  }, []); // 空依赖数组,effect 只执行一次

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

点击按钮后,页面显示的数字在增加,但控制台始终输出 0。

原因分析

// useEffect 的回调在组件首次渲染时创建
// 此时 count = 0,回调函数形成闭包,捕获了这个值
useEffect(() => {
  // 这个函数"记住"了创建时的 count 值
  const timer = setInterval(() => {
    console.log(count); // 闭包中的 count 永远是 0
  }, 1000);
  return () => clearInterval(timer);
}, []);

解决方案

方案一:添加依赖

useEffect(() => {
  const timer = setInterval(() => {
    console.log('当前 count:', count);
  }, 1000);

  return () => clearInterval(timer);
}, [count]); // count 变化时重新创建定时器

缺点:每次 count 变化都会清除并重建定时器。

方案二:使用 useRef

import { useState, useEffect, useRef } from 'react';

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

  // 同步更新 ref
  useEffect(() => {
    countRef.current = count;
  }, [count]);

  useEffect(() => {
    const timer = setInterval(() => {
      // ref.current 始终是最新值
      console.log('当前 count:', countRef.current);
    }, 1000);

    return () => clearInterval(timer);
  }, []);

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

方案三:函数式更新(适用于需要基于旧值更新的场景)

useEffect(() => {
  const timer = setInterval(() => {
    // 函数式更新可以拿到最新的 state
    setCount(prevCount => {
      console.log('当前 count:', prevCount);
      return prevCount + 1;
    });
  }, 1000);

  return () => clearInterval(timer);
}, []);

方案四:使用 useCallback + 依赖

import { useState, useEffect, useCallback } from 'react';

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

  const logCount = useCallback(() => {
    console.log('当前 count:', count);
  }, [count]);

  useEffect(() => {
    const timer = setInterval(logCount, 1000);
    return () => clearInterval(timer);
  }, [logCount]);

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

关键点

  • 闭包陷阱的本质:函数创建时捕获了当时的变量值,后续变量更新不会影响已捕获的值
  • useRef 的 .current 是可变的,不会触发重渲染,适合存储需要跨渲染周期访问的最新值
  • 函数式更新 setState(prev => ...) 可以获取最新 state,但只适用于需要更新 state 的场景
  • 正确设置依赖数组是最直接的解决方案,但要注意可能带来的性能影响
  • 事件监听、定时器、订阅等场景最容易遇到闭包陷阱