React 系列:ahooks 源码解析之 useUnmount 为何需要 useLatest

背景

翻阅 useMount 和 useUnmount 的源码我们会有一个疑惑:为什么 useUnmount 要用 useRef(useLatest 底层使用的是 useRef),而 useMount 无需。

useMount 源码:

tsx 复制代码
import { useEffect } from 'react';
import { isFunction } from '../utils';
import isDev from '../utils/isDev';

const useMount = (fn: () => void) => {
  if (isDev) {
    if (!isFunction(fn)) {
      console.error(
        `useMount: parameter \`fn\` expected to be a function, but got "${typeof fn}".`,
      );
    }
  }

  useEffect(() => {
    fn?.();
  }, []);
};

export default useMount;

简写为:

tsx 复制代码
import { useEffect } from 'react';

const useMount = (fn: () => void) => {
  useEffect(() => {
    fn();
  }, []);
};

export default useMount;

useUnmount 就难以理解些,需要 useLatest:

tsx 复制代码
import { useEffect } from 'react';
import useLatest from '../useLatest';
import { isFunction } from '../utils';
import isDev from '../utils/isDev';

const useUnmount = (fn: () => void) => {
  if (isDev) {
    if (!isFunction(fn)) {
      console.error(`useUnmount expected parameter is a function, got ${typeof fn}`);
    }
  }

  const fnRef = useLatest(fn);

  useEffect(
    () => () => {
      fnRef.current();
    },
    [],
  );
};

export default useUnmount;

useLatest 源码:

tsx 复制代码
import { useRef } from 'react';

function useLatest<T>(value: T) {
  const ref = useRef(value);
  ref.current = value;

  return ref;
}

export default useLatest;

简写为:

tsx 复制代码
import { useEffect, useRef } from 'react';

const useUnmount = (fn: () => any): void => {
  const fnRef = useRef(fn);

  fnRef.current = fn;

  useEffect(() => () => fnRef.current(), []);
};

我们看看业界其他流行的 hooks 库 react-use useMount

tsx 复制代码
import useEffectOnce from './useEffectOnce';

const useMount = (fn: () => void) => {
  useEffectOnce(() => {
    fn();
  });
};

export default useMount;

---

// react-use/blob/master/src/useEffectOnce.ts
import { EffectCallback, useEffect } from 'react';

const useEffectOnce = (effect: EffectCallback) => {
  useEffect(effect, []);
};

export default useEffectOnce;

useUnmount:

tsx 复制代码
import { useRef } from 'react';
import useEffectOnce from './useEffectOnce';

const useUnmount = (fn: () => any): void => {
  const fnRef = useRef(fn);

  // update the ref each render so if it change the newest callback will be invoked
  fnRef.current = fn;

  useEffectOnce(() => () => fnRef.current());
};

export default useUnmount;

发现相同规律 useUnmount 必须使用 ref。且特意注释使用原因:update the ref each render so if it change the newest callback will be invoked

难道不用不行吗?

tsx 复制代码
import { useEffect } from 'react';

const useUnmount = (fn: () => any): void => 
  useEffect(() => {
    return () => {
      fn()
    }
  }, []);
};

解疑 1:为什么 useUnmount 为何需要 useRef?✔️

在实际工程项目中,卸载时需要最新回调函数的情况非常常见,这确实有实质性的意义。回调函数的变化往往与 组件状态/Props 变化闭包问题动态行为调整 相关。下面通过具体场景说明:


1. 回调函数依赖组件状态或 Props

这是最常见的场景。如果卸载时的回调函数依赖了组件的最新状态或 Props,但未正确更新,就会导致闭包问题(stale closure)。

示例:记录卸载时的数据

tsx 复制代码
function UserProfile({ userId }) {
  const [data, setData] = useState(null);

  // 回调函数依赖了最新的 `userId` 和 `data`
  const logUnmount = () => {
    console.log(`User ${userId} unmounted with data:`, data);
  };

  useUnmount(logUnmount); // 必须确保卸载时调用的是最新的 logUnmount

  // ... 数据加载逻辑 ...
}
  • 如果 userIddata 变化,但卸载时调用的回调是旧的,就会记录错误的信息。
  • 解决方法 :用 useRef 保持最新回调。

2. 动态行为调整

某些情况下,组件的卸载行为需要根据运行时条件动态调整。

示例:根据权限清理资源

tsx 复制代码
function DocumentEditor() {
  const [userRole, setUserRole] = useState('viewer');

  // 卸载时的清理逻辑可能因用户角色而异
  const cleanup = () => {
    if (userRole === 'admin') {
      // 管理员需要额外清理敏感数据
      clearSensitiveData();
    }
    saveDraft(); // 普通用户只保存草稿
  };

  useUnmount(cleanup); // 必须用最新的 cleanup
}
  • 如果用户权限从 viewer 变为 admin,但卸载时调用了旧的 cleanup,就会漏掉敏感数据清理。

3. 避免内存泄漏

如果回调函数引用了外部变量(如 API 订阅、DOM 事件),但未更新到最新版本,可能导致资源未正确释放。

示例:取消订阅事件

tsx 复制代码
function ChatRoom() {
  const [roomId, setRoomId] = useState('general');

  const handleUnmount = () => {
    // 必须根据最新的 roomId 取消订阅
    unsubscribe(roomId); 
  };

  useUnmount(handleUnmount);

  // 如果 roomId 变化但卸载时用了旧回调,会导致订阅泄漏!
}

若不用 ref

tsx 复制代码
useEffect(() => () => fn(), []); // 问题:fn 可能过时
  • 如果 fn 依赖了外部变量(如 state/props),而依赖项数组为空,卸载时调用的会是 初始渲染时的旧 fn,引发闭包问题。
  • React 官方建议 :如果 Hook 接受回调函数作为参数,应该确保回调是最新的(参考 useEvent RFC 的讨论)。

实际工程中的解决方案

  1. 推荐模式 :使用 useRef 保存最新回调(原始代码的实现)。
  2. 工具库方案 :像 ahooksuseUnmountreact-useuseUnmount 均采用此设计。
  3. 未来方案 :React 可能推出 useEvent (暂未正式发布 rfcs-0000-useevent.md),专门解决此类问题。目前可以使用 react-use www.reactuse.com/effect/usee... 或 ahooks useLatest,或者干脆自行封装,

在工程实践中,除非能绝对保证回调函数不会变化 ,否则使用 useRef 保存最新值是更安全的选择。

解疑 2:为什么 useMount 无需 useRef?🚫

二者执行时机不同,渲染完毕执行,此时记录的是渲染之前的值,记录完毕立马执行,不存在过时的说法。

而 useUnmount 实在渲染完毕后执行,不能仅仅记住渲染那一刻的"快照"而必须是"Latest"的。

相关推荐
Cacciatore->1 小时前
React 基本介绍与项目创建
前端·react.js·arcgis
摸鱼仙人~1 小时前
React Ref 指南:原理、实现与实践
前端·javascript·react.js
贵沫末1 小时前
React——基础
前端·react.js·前端框架
爱学习的茄子2 小时前
AI驱动的单词学习应用:从图片识别到语音合成的完整实现
前端·深度学习·react.js
10年前端老司机2 小时前
在React项目中如何封装一个可扩展,复用性强的组件
前端·javascript·react.js
sophie旭2 小时前
《深入浅出react开发指南》总结之 10.1 React运行时总览
前端·react.js·源码阅读
轻语呢喃3 小时前
React智能前端:从零开始写的图片分析页面实战
前端·react.js·aigc
MiyueFE3 小时前
每个前端开发者都应该掌握的几个 ReactJS 概念
前端·react.js
旧时光_4 小时前
Zustand 状态管理库完全指南 - 进阶篇
前端·react.js
Sun_light6 小时前
6个你必须掌握的「React Hooks」实用技巧✨
前端·javascript·react.js