用 React 19.2 Activity 保留页面状态前,先弄清 hidden 模式做了什么

多页签后台有一个反复出现的需求:用户从编辑页切到列表页,再切回来时,表单输入、滚动位置和局部筛选不能丢。最直接的写法是条件渲染:

jsx 复制代码
{activeTab === "editor" && <Editor />}

问题也很直接。条件变成 false 后,Editor 被卸载,组件内部状态随之消失。把所有状态提升到全局可以解决一部分问题,却会让临时 UI 状态进入业务 store。用 CSS 把节点隐藏则会保留状态,但副作用、订阅和更新仍可能继续运行。

React 19.2 提供了 <Activity />。它在当前版本有 visiblehidden 两种模式:

jsx 复制代码
import { Activity } from "react";

function Workspace({ activeTab }) {
  return (
    <>
      <Activity mode={activeTab === "editor" ? "visible" : "hidden"}>
        <Editor />
      </Activity>
      <Activity mode={activeTab === "list" ? "visible" : "hidden"}>
        <ResultList />
      </Activity>
    </>
  );
}

根据 React 官方说明,hidden 会隐藏子树、卸载 Effect,并把该子树的更新推迟到 React 没有更紧急工作的时候。切回 visible 后,内容重新显示,Effect 再次挂载,更新恢复正常优先级。它和"组件继续在后台照常运行,只是看不见"并不是一回事。

这个区别决定了 Activity 适合什么。编辑器、复杂筛选面板、可返回的路由页面,通常希望保留 React 状态和已经构建的 UI,同时不让隐藏页面的订阅长期占用资源。Activity 正好位于卸载和 CSS 隐藏之间。它也可以提前渲染用户很可能访问的下一页,让数据、样式和图片在后台准备,但不抢占眼前交互。

第一类迁移风险来自 Effect。假设组件通过 Effect 订阅 WebSocket:

jsx 复制代码
useEffect(() => {
  const unsubscribe = socket.subscribe(roomId, onMessage);
  return unsubscribe;
}, [roomId]);

进入 hidden 后,这个订阅会清理。再次显示时会重新订阅。如果业务要求页面隐藏期间仍持续接收并缓存消息,就不能把数据生命周期完全交给这个页面 Effect。更合理的做法是把连接和消息缓存放到页面之外的服务层,页面 Effect 只负责展示层订阅。Activity 会迫使我们分清"界面不可见时应该暂停的工作"和"业务上必须持续的工作"。

第二类风险是误把 Activity 当成无限缓存。保留十几个大型页面的状态和节点仍会占用内存。React 可以降低隐藏更新的优先级,却不会替应用决定何时彻底释放页面。实际项目最好给页签设置上限,区分临时切换和长期离开。最近使用的少量页面放进 Activity,超过数量后真正卸载,往往比把所有历史页面永久隐藏更稳。

第三类风险是没有测量就预渲染。提前渲染下一页能缩短切换等待,但也会产生请求、解析响应、创建对象和图片解码。如果"下一页"预测不准,后台工作只是在消耗流量。可以先记录页面转移概率,只对高概率路径预渲染,并在慢网、低性能设备和节流模式下关闭。React 19.2 还提供 Performance Tracks,可以在 Chrome DevTools 的性能记录中观察调度优先级、渲染和 Effect 工作,适合判断隐藏子树是否挤占了可见交互。

迁移时不建议一口气改路由层。先挑一个状态丢失最明显的双页签场景,记录三个指标:切换后的可交互时间、隐藏期间的网络与 CPU 活动、往返切换后的内存变化。然后检查所有 Effect 的清理与重建是否符合业务预期。若组件依赖"只在首次挂载执行一次"的副作用,还要把这类隐含假设改掉,因为可见性切换会让 Effect 再次挂载。

Activity 也不能替代服务端状态缓存。查询结果、乐观更新和失效策略仍应由数据层处理。它保留的是 React 子树及其界面状态,并调整隐藏工作的生命周期和优先级。把这条边界守住,组件不会因为"想保留一个输入框"而背上整套后台任务。

我更愿意把 Activity 看成一种页面生命周期工具。它解决的不是"怎么把 DOM 藏起来",而是用户暂时离开一个界面时,哪些状态应保留,哪些副作用应暂停,哪些数据应移到更长寿命的层。先回答这三个问题,再替换条件渲染,代码会清楚很多。

相关推荐
小林ixn1 小时前
React + TypeScript 实战:从“类型体操”到“数据持久化”,一次讲透组件通信与副作用管理
前端·react.js·typescript
何时梦醒1 小时前
React + TypeScript 企业级开发实战:从零搭建到组件架构演进
前端·react.js·全栈
不好听6131 小时前
从困惑到理解:React 父子组件通信的三种写法
react.js·typescript
阿黎梨梨1 小时前
React + TypeScript 入门:从组件类型到数据流设计
react.js
烬羽1 小时前
React 状态归属:两个版本的用户名编辑器,告诉你 state 该放哪
react.js·typescript·前端框架
名字还没想好☜2 小时前
React createPortal 实战:模态框逃出 overflow:hidden、事件冒泡与焦点管理
前端·javascript·react.js·ecmascript·react·createportal
GitLqr21 小时前
提升 Web 开发效率:我常用的 10 个 TypeScript 实战技巧
vue.js·react.js·typescript
无人生还1 天前
从 Vue3 到 React · 快速上手系列第 6 篇:状态管理 useState 与 useReducer
前端·vue.js·react.js
张元清1 天前
React usePrevious Hook:追踪上一次的 State 和 Props(2026)
javascript·react.js