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"的。

相关推荐
__不想说话__3 分钟前
面试官问我React状态管理,我召唤了武林群侠传…
前端·react.js·面试
JiangJiang12 分钟前
🎯 Vue 人看 useReducer:比 useState 更强的状态管理利器!
前端·react.js·面试
涵信2 小时前
第二十节:项目经验-描述一个React性能优化案例
前端·react.js·性能优化
方方洛3 小时前
组件是怎样写的(1):虚拟列表-VirtualList
前端·vue.js·react.js
北京小伙_盼4 小时前
【全新体验】音乐与有声书结合H5客户端APP
android·前端·react.js
sunbyte5 小时前
Three.js + React 实战系列-3D 个人主页 :完成 Navbar 导航栏组件
开发语言·javascript·react.js
小钰能吃三碗饭9 小时前
第十二篇:【React + AI】深度实践:从 LLM 集成到智能 UI 构建
前端·react.js·aigc
Nu119 小时前
前端大屏原理系列:拖拽组件到页面
前端·react.js·开源
前端大白话9 小时前
震惊!原来在React中用useRef Hook实现定时器这么简单!手把手教你告别内存泄漏
前端·react.js