前端进阶之旅前端进阶之旅
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
首页程序员面试题库React useRef 保存前一个已提交值
ReReactHooks 与复用

React 用 ref 保存前一个值时,“上一次渲染”和“上一次提交”有什么区别?

先定义历史记录的时刻,再谈 previous;没有提交的渲染尝试不应自动变成业务历史。

前端进阶之旅 · 一题精讲更新于 2026.09.06
React#Hooks 与复用
先看核心答案读代码示例
理解线索

历史值需要一个明确的记录规则

  1. 初始记录决定第一次输入是否有可比较的前值
  2. 提交观察Effect 比较本次输入与最近记录
  3. 业务历史需要保留每个动作时使用独立事件记录

示例记录状态变化通知,不把 ref 读入 JSX,因此不会依赖可变历史在渲染中的偶然时机。

核心回答

先记住这个答案

ref 可以跨渲染保存数据,但它记录的是你实际写入的那一次值,不会自动代表所有意义上的“上一次”。若在 Effect 中记录,它更准确地表示上次 Effect 观察到的已提交值;没有提交的尝试和批处理中间状态未必被记录。用于 Effect 内比较或发送变化通知时,可以在那里读取旧值并更新记录。若前一个值要参与 JSX 显示,应在 state、reducer 或上层历史结构中显式建模,不推荐通过渲染期间读取随后被 Effect 改写的 ref 来生成界面。

  • 明确历史是事件、提交还是 Effect 观察序列
  • 比较与写入都在同一个合适阶段完成
  • 页面要展示历史时把历史建模为可渲染状态

在 Effect 内完成比较和写入

状态观察器最初收到 idle,不把首次挂载解释成一次从未知状态的业务变化。后来 status 变为 loading,再变为 done,Effect 分别记录 idle 到 loading、loading 到 done,并把 current 更新为本次已经观察到的值。

onChange 也属于 Effect 读取的响应式输入,因此需要列为依赖。它的身份变化可能让 Effect 重新执行,但 Object.is 比较发现状态没变时不发送重复变化通知;这区分了 Effect 执行与业务状态真正变化。

在 Effect 中记录已经观察到的状态变化TSX
import { useEffect, useRef } from 'react';

type Props = {
  status: string;
  onChange: (before: string, after: string) => void;
};
export default function StatusObserver({ status, onChange }: Props) {
  const previous = useRef(status);
  useEffect(() => {
    const before = previous.current;
    previous.current = status;
    if (!Object.is(before, status)) onChange(before, status);
  }, [status, onChange]);
  return <p>当前状态:{status}</p>;
}

首次挂载不发变化通知;后续已观察到的不同 status 才成对回传。JSX 只读取 status,没有在渲染阶段读取 previous.current;这个记录器不是持久化事件日志。

批处理中间值未必成为这份历史

如果一个事件里先请求 loading 又直接替换成 done,最终只提交 done,那么观察器可能只看到 idle 到 done。它没有承诺记录每次 setter 的入队值,因此不能拿来审计用户究竟发出了多少个动作。

需要每次点击、每次任务尝试或每条消息都保留时,应在动作发生的边界记录事件标识和输入。展示状态是一种当前投影,事件历史则是另一类数据;不要把 Effect 的调用序列硬当作完整业务事件序列。

渲染历史应与当前值一起更新

如果页面要显示涨跌趋势、上一条报价或撤销内容,可把 previous 与 current 放进同一状态结构,更新时同时推进。这样 JSX 读取的是明确状态,而不是某个会在提交后被改写的共享 ref 容器。

还要定义相同值算不算一次历史推进、首次值显示什么、组件重新挂载是否清空记录,以及对象历史是否需要保留不可变版本。仅仅复制一个通用 usePrevious 函数名,无法替你决定这些业务规则。

回答前,多想一步

容易答错的地方

Effect 写入的 ref 一定是每次 render 的前一个值
没有提交的渲染和被合并的中间状态未必运行对应 Effect,记录只代表实际观察过的序列;必须按用途精确命名和解释。
ref 保存旧对象就能永久保留旧字段内容
如果对象随后被原地修改,旧引用看到的内容也会改变。需要历史版本时必须保护不可变数据,不能把引用容器当作自动快照复制器。
试着用自己的话回答

面试官还会怎么问?

Strict Mode 会让这个示例首次多发一次变化吗?

示例以当前 status 初始化,并在 Effect 中比较相同值,额外建立检查不会凭空制造状态变化。但真实外部通知仍应按业务需求处理幂等与重挂载。

为什么不在 render 最后一行更新 previous.current?

渲染可能被重复或放弃,这样会把未提交尝试写进历史并影响现有回调。应在合适阶段记录,或把需要渲染的前后值显式保存到状态里。

组件卸载再挂载后还能保留历史吗?

组件内的 ref 会随实例重新建立,不能承担跨实例持久历史。若业务需要保留,应把记录提升到更长生命周期的状态或存储层并定义清理规则。

从一道题,走向一组知识

把知识连起来

Hooks 与复用

React 为什么不应在渲染时读写 ref.current,惰性初始化为什么是例外?

了解渲染时读写可变引用为什么会破坏纯度

状态与渲染

React 事件里的多个 setState 怎样批处理,为什么不能只数 render 日志?

批处理可能使中间状态不成为可观察的提交

状态与渲染

React 嵌套对象怎样不可变更新,为什么只写展开运算符还会共享旧引用?

保存历史对象必须避免后续原地修改旧版本

参考资料

  • React:useRef
  • React:useEffect
  • React:Storing information from previous renders

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 在 Effect 内完成比较和写入
  3. 批处理中间值未必成为这份历史
  4. 渲染历史应与当前值一起更新
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑