发布于 2026-01-06 1 阅读
0

如何使用 Ref 解决 React 性能问题

如何使用 Ref 解决 React 性能问题

以及我们是如何阻止 React Context 重新渲染所有内容的。

Refs 是 React 中一个很少使用的功能。如果你读过官方的 React 指南,就会发现它被介绍为一种跳出典型 React 数据流的“逃生舱”,并警告要谨慎使用,而且它主要被宣传为访问组件底层 DOM 元素的正确方法。

但除了 Hooks 的概念之外,React 团队还引入了useRefHook,它扩展了这一功能:

useRef()它的用途不仅限于ref属性本身。它还可以方便地保存任何可变值,类似于在类中使用实例字段的方式。

虽然我在新的 Hook API 发布时忽略了这一点,但事实证明它非常有用。

👉点击此处直接查看解决方案和代码片段

问题

我是一名软件工程师,正在开发Rowy,这是一个开源的 React 应用,它将电子表格的用户界面与 Firestore 和 Firebase 的强大功能相结合。它的一个关键特性是侧边抽屉,这是一个类似表单的界面,用于编辑单行数据,可以滑到主表格上方。

用户选择单元格并打开侧边抽屉的屏幕录像

当用户点击表格中的某个单元格时,可以打开侧边栏来编辑该单元格对应的行。换句话说,侧边栏中显示的内容取决于当前选中的行——这个信息应该存储在状态中。

将此状态放置在侧边抽屉组件本身是最合乎逻辑的位置,因为当用户选择不同的单元格时,它应该影响侧边抽屉。然而:

  • 我们需要在表格组件中设置react-data-grid这个状态。我们用它来渲染表格本身,它接受一个回调属性,当用户选择一个单元格时,该属性会被调用。目前,这是响应该事件的唯一方法。

  • 但是侧抽屉和桌子组件是同级组件,所以它们不能直接访问彼此的状态。

React 的建议是将此状态提升到组件最近的共同祖先,在本例中是 `<component>` TablePage。但我们决定不在此处移动状态,因为:

  1. TablePage它不包含任何状态,主要用作桌子和侧抽屉组件的容器,这两个组件都没有接收任何属性。我们更倾向于保持这种状态。

  2. 我们已经通过位于组件树根部附近的上下文共享了大量“全局”数据,因此我们觉得将此状态添加到该中央数据存储中是有意义的。

附注:即使我们将状态放入其中TablePage,我们仍然会遇到下面同样的问题。

问题在于,每当用户选择一个单元格或打开侧边栏时,全局上下文的更新都会导致整个应用程序重新渲染。这包括主表格组件,该组件可能同时显示数十个单元格,每个单元格都有自己的编辑器组件。这将导致大约650 毫秒的渲染时间(!),足以让侧边栏的打开动画出现明显的延迟。

侧抽屉打开动画延迟的屏幕录像

请注意点击打开按钮到侧边抽屉打开动画之间的延迟。

其背后的原因是上下文的一个关键特性——这也是为什么在 React 中使用上下文比使用全局 JavaScript 变量更好的原因:

当 Provider 的valueprop 发生变化时,所有作为 Provider 后代的消费者都会重新渲染。

虽然到目前为止,这种对 React 状态和生命周期的 Hook 一直对我们很有帮助,但现在看来,我们似乎搬起石头砸了自己的脚。

顿悟时刻

我们首先探索了几种不同的解决方案(来自Dan Abramov关于此问题的帖子),最终确定了以下方案useRef

  1. 拆分上下文,即创建一个新的上下文SideDrawerContext
    表格仍然需要使用新的上下文,而新的上下文在侧边抽屉打开时仍然会更新,导致表格不必要地重新渲染

  2. 将表格组件包裹在 `<table>`React.memo或 `<div>`中useMemo
    表格仍然需要调用 `<div>`useContext来访问侧边抽屉的状态,而这两个 API 都无法阻止它导致重新渲染

  3. react-data-grid对用于渲染表格的组件进行记忆化处理
    。 这会增加代码的冗余度。我们还发现,这样做会阻止必要的重新渲染,导致我们不得不花费更多时间修复或完全重构代码,仅仅是为了实现侧边栏抽屉功能。

在反复阅读 Hook API 文档后useMemo,我终于注意到了这一点useRef

useRef()它的用途不仅限于ref属性本身。它还可以方便地保存任何可变值,类似于在类中使用实例字段的方式。

更重要的是:

useRef 当其内容发生变化时,不会通知您。修改该 .current属性不会导致重新渲染

就在那时,我突然意识到:

我们不需要存储侧边抽屉的状态——我们只需要一个指向设置该状态的函数的引用。

解决方案

  1. 将开放状态和单元格状态保存在旁边的抽屉里。

  2. 创建对这些状态的引用并将其存储在上下文中。

  3. 当用户点击单元格时,使用表格中的引用调用设置状态函数(在侧边抽屉内)。

表示上述列表的图表

以下代码是 Rowy 上使用的代码的简化版本,其中包含 ref 的 TypeScript 类型:

import { SideDrawerRef } from 'SideDrawer'
export function ProjectContextProvider({ children }) {
const sideDrawerRef = useRef<SideDrawerRef>();
return (
<ProjectContext.Provider value={{ sideDrawerRef }}>
{children}
</ProjectContext.Provider>
)
}
export type SideDrawerRef = {
cell: SelectedCell;
setCell: React.Dispatch<React.SetStateAction<SelectedCell>>;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
};
export default function SideDrawer() {
const classes = useStyles();
const { tableState, dataGridRef, sideDrawerRef } = useProjectContext();
const [cell, setCell] = useState<SelectedCell>(null);
const [open, setOpen] = useState(false);
sideDrawerRef.current = { cell, setCell, open, setOpen };
...
}
view raw SideDrawer.tsx hosted with ❤ by GitHub

附注:由于函数组件在重新渲染时会运行整个函数体,因此每当状态更新(并导致重新渲染)时cellopen都会sideDrawerRef始终包含最新的值 .current

事实证明,这个方案是最佳的,因为:

  1. 当前单元格和打开状态存储在侧抽屉组件本身中,这是最合乎逻辑的放置位置。

  2. 表格组件在需要可以访问其同级组件的状态。

  3. 当当前单元格或打开状态更新时,只会触发侧边抽屉组件的重新渲染,而不会触发应用程序中的任何其他组件的重新渲染。

你可以在这里这里看到 Rowy 是如何使用它的

何时使用Ref

但这并不意味着你应该在所有构建中都使用这种模式。它最适合用于需要在特定时间访问或更新另一个组件的状态,但你的组件并不依赖于该状态或基于该状态进行渲染的情况。React 的核心概念——状态提升和单向数据流——已经足以涵盖大多数应用程序架构。


感谢阅读!您可以在下方了解更多关于Rowy 的信息,也可以在 Twitter 上关注我@nots_dney

GitHub 标志 rowyo / rowy

低代码后端平台。在类似电子表格的用户界面上管理数据库,并在浏览器中使用 JS/TS 构建云函数工作流。

✨ 类似 Airtable 的数据库管理界面 ✨ 无需编写代码,即可构建任何自动化流程 ✨

连接数据库,无需离开浏览器即可使用低代码创建云函数。
专注于构建您的应用程序。适用于 Firebase 和 Google Cloud 的低代码

在线演示🛝

💥 在实时演示环境中体验 Rowy 💥

特色✨

20211004-RowyWebsite.mp4

功能强大的 Firestore 电子表格界面

  • Firestore 的 CMS
  • CRUD 操作
  • 批量导入或导出数据 - CSV、JSON、TSV
  • 按行值排序和筛选
  • 锁定、冻结、调整大小、隐藏和重命名列
  • 同一系列的多种视图

利用云函数和现成的扩展程序实现自动化

  • 基于字段级数据变更构建云函数工作流
    • 使用任何 NPM 模块或 API。
  • 使用预置代码块连接到您最喜欢的工具,或者创建您自己的代码块。
    • SendGrid、Algolia、Twilio、BigQuery 等

丰富且灵活的数据字段





文章来源:https://dev.to/notsidney/how-to-useref-to-fix-react-performance-issues-e8p