首页 / React 19 入门教程 / 依赖与清理

React 19 入门教程

依赖与清理

本教程共 50 篇 · 第 28 篇 · 更新于 2026-07-29 · 约 7 分钟阅读

ReactuseEffect清理函数依赖数组竞态

28. 依赖与清理

本节目标:掌握依赖数组的完整规则,学会写清理函数,避免无限循环和竞态问题。

依赖数组规则

effect 里用到的所有响应式值都要放进依赖数组。

响应式值包括:

  • props
  • state
  • 组件内声明的变量和函数
function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);
  const apiKey = 'xxx'; // 常量,不是响应式值

  useEffect(() => {
    fetch(`/api/search?q=${query}&page=${page}&key=${apiKey}`)
      .then(res => res.json())
      .then(data => setResults(data));
  }, [query, page]); // apiKey 不是响应式值,不需要放
}

ESLint 会帮你检查

eslint-plugin-react-hooks 后,react-hooks/exhaustive-deps 规则会自动提示遗漏的依赖。别忽略它。

空依赖数组

effect 不依赖任何响应式值时,用空数组:

useEffect(() => {
  const canvas = document.getElementById('canvas');
  // 初始化画布,只执行一次
}, []);

空数组意味着”只在挂载时执行,卸载时清理”。

没有依赖数组

不设依赖数组,每次渲染后都执行:

useEffect(() => {
  console.log('每次渲染都跑');
});

大多数情况这是bug。如果你确实需要,想清楚为什么。

清理函数

有些 effect 需要”撤销”之前的操作。返回一个清理函数:

useEffect(() => {
  // 开始
  const timer = setInterval(() => tick(), 1000);

  // 清理(返回这个函数)
  return () => {
    clearInterval(timer);
  };
}, []);

清理函数的执行时机:

  • 重新同步前:依赖变化时,先清理旧的,再执行新的
  • 卸载时:组件移除时,执行清理

定时器的清理

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

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

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

  return <p>{count}</p>;
}

不设清理函数,组件卸载后定时器还在跑,会尝试更新不存在的状态,导致内存泄漏。

事件监听的清理

function WindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

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

    window.addEventListener('resize', handleResize);

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

  return <p>{size.width} x {size.height}</p>;
}

不清理事件监听,组件卸载后回调还在,会内存泄漏。

竞态问题

快速切换时,后发的请求可能先返回,先发的反而后返回。结果被覆盖:

useEffect(() => {
  fetch(`/api/users/${userId}`)
    .then(res => res.json())
    .then(data => setUser(data));
}, [userId]);

用户从用户A切换到B,A的请求可能比B晚回来,页面最后显示A的信息。

解决方案:用 AbortController 取消旧请求:

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/users/${userId}`, { signal: controller.signal })
    .then(res => res.json())
    .then(data => setUser(data))
    .catch(err => {
      if (err.name !== 'AbortError') throw err;
    });

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

AbortController 是浏览器原生 API

不仅能取消 fetch,还能取消其他异步操作。是现代浏览器都支持的。

对象和函数作为依赖

每次渲染都创建新的对象/函数引用,导致 effect 无限执行:

// 问题:每次渲染 options 都是新对象
useEffect(() => {
  fetch('/api', { body: JSON.stringify(options) });
}, [options]); // 无限循环!

// 解决:提取基本类型依赖
const { limit, offset } = options;
useEffect(() => {
  fetch('/api', { body: JSON.stringify(options) });
}, [limit, offset]);

// 或者用 useMemo 缓存对象
const options = useMemo(() => ({ limit, offset }), [limit, offset]);

函数同理:

// 问题:每次渲染 handleFetch 都是新函数
useEffect(() => {
  handleFetch();
}, [handleFetch]);

// 解决:用 useCallback 缓存函数
const handleFetch = useCallback(() => {
  // ...
}, [deps]);

依赖速查

// 不依赖任何值
useEffect(() => { ... }, []);

// 依赖 props
useEffect(() => { ... }, [userId, roomId]);

// 依赖 state
useEffect(() => { ... }, [query, page]);

// 依赖 props + state
useEffect(() => { ... }, [props.id, state.filter]);

本节要点

  • effect 里所有响应式值都要放进依赖数组
  • 清理函数在重新同步前和卸载时执行
  • 定时器、事件监听、网络请求都要清理
  • 竞态问题用 AbortController 解决
  • 对象/函数依赖用 useMemo/useCallback 缓存