react-admin 实时订阅实战:深入掌握 `useSubscribeToRecord` 单记录事件订阅 Hook
2026/9/21 3:27:04 网站建设 项目流程

react-admin 实时订阅实战:深入掌握useSubscribeToRecord单记录事件订阅 Hook

【免费下载链接】react-adminA frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design项目地址: https://gitcode.com/gh_mirrors/re/react-admin

useSubscribeToRecord是 react-admin 企业版ra-realtime包中基于useSubscribe的特化 Hook,用于订阅某个单一记录resource/[resource]/[recordId]topic)上发布的实时事件。本文结合仓库中的官方文档与ra-realtime的底层机制,完整讲解该 Hook 的用法、参数、回调语义,以及它与useSubscribeuseSubscribeToRecordListuseGetOneLive的取舍,帮助你在协同编辑、并发冲突提示、实时通知等场景中落地实时能力。

背景:ra-realtime的发布/订阅模型

react-admin 为多人在线协作场景提供了实时(Realtime)能力:允许多个用户并行工作、发布与订阅实时事件、在他人推送变更时自动刷新视图、向终端用户发送事件通知,并通过锁机制防止同一资源被多人同时编辑。这些能力由@react-admin/ra-realtime包提供,属于 Enterprise Edition(详见 docs/Realtime.md)。

其核心是一个发布/订阅(pub/sub)机制

// 发布侧 const [publish] = usePublish(); publish(topic, event); // 订阅侧 useSubscribe(topic, callback);

ra-realtime在此基础上提供了一组高级 Hook 与组件,useSubscribeToRecord就是其中之一,与其并列的还有useSubscribeuseSubscribeCallbackuseSubscribeToRecordListusePublish(完整列表见 docs/Realtime.md)。

在底层,实时能力完全复用了 react-admin 的dataProvider适配器模式。要启用实时功能,dataProvider需要实现三个新方法(详见 docs/RealtimeDataProvider.md):

  • subscribe(topic, callback)
  • unsubscribe(topic, callback)
  • publish(topic, event)(可选,发布通常由服务端完成)

ra-realtime支持 Mercure、API Platform、Supabase、Socket.IO、Ably 等多种实时基础设施,也可以基于本地变量手写自定义适配器(仓库文档给出了基于内存数组的subscribe/unsubscribe/publish参考实现,参见 docs/RealtimeDataProvider.md 的 "Writing a Custom Adapter" 一节)。

安装与前置条件

ra-realtime是 React-Admin Enterprise Edition 的一部分,托管在私有 npm registry 中,需要订阅 Enterprise Edition 计划才能安装。按照 docs/Realtime.md 的说明安装:

npm install --save @react-admin/ra-realtime # 或 yarn add @react-admin/ra-realtime

同时需要配置一个支持实时订阅的dataProvider,具体要求见 docs/RealtimeDataProvider.md。

基本用法:订阅单记录事件

useSubscribeToRecord与通用版useSubscribe的最大区别在于:它只需要传一个回调函数,资源名(resource)和记录 id 都会从当前上下文自动推断。回调会在resource/[resource]/[recordId]topic 上发布事件时被执行。

以下示例实现了一个典型的并发编辑冲突提示组件:当记录被他人更新(收到edited事件)时,如果当前用户正在编辑表单(isDirty),就弹出一个对话框提醒冲突,并允许用户选择保留自己的修改还是拉取对方的最新数据;如果表单干净,则直接refetch()刷新数据(原文档 docs/useSubscribeToRecord.md 的完整示例):

import { useState } from 'react'; import { useEditContext, useFormContext } from 'react-admin'; import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, } from '@mui/material'; import { useSubscribeToRecord } from '@react-admin/ra-realtime'; const WarnWhenUpdatedBySomeoneElse = () => { const [open, setOpen] = useState(false); const [author, setAuthor] = useState<string | null>(null); const handleClose = () => { setOpen(false); }; const { refetch } = useEditContext(); const refresh = () => { refetch(); handleClose(); }; const { formState: { isDirty }, } = useFormContext(); useSubscribeToRecord((event: Event) => { if (event.type === 'edited') { if (isDirty) { setOpen(true); setAuthor(event.payload.user); } else { refetch(); } } }); return ( <Dialog open={open} onClose={handleClose} aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" > <DialogTitle id="alert-dialog-title"> Post Updated by {author} </DialogTitle> <DialogContent> <DialogContentText id="alert-dialog-description"> Your changes and their changes may conflict. What do you want to do? </DialogContentText> </DialogContent> <DialogActions> <Button onClick={handleClose}>Keep my changes</Button> <Button onClick={refresh}> Get their changes (and lose mine) </Button> </DialogActions> </Dialog> ); }; const PostEdit = () => ( <Edit> <SimpleForm> <TextInput source="id" disabled /> <TextInput source="title" /> <TextInput source="body" multiline /> <WarnWhenUpdatedBySomeoneElse /> </SimpleForm> </Edit> );

上下文推断与组件摆放位置

useSubscribeToRecord会分别从ResourceContextRecordContext读取当前的资源名与记录 id。上面的例子中,当应用收到resource/books/123topic 上的事件时,会触发通知——其中books来自ResourceContext123来自RecordContext

一个容易踩坑的细节是组件摆放位置:<Show><Edit>等页面组件会创建RecordContext,因此useSubscribeToRecord必须放在其子组件中才能读取到上下文,而不能放在页面组件本身。原文档明确指出:

In the example above,<Show>creates theRecordContext— that's why theuseSubscribeToRecordhook is used in its child component instead of in the<BookShow>component.

useSubscribe一样,当组件卸载时,useSubscribeToRecord会自动从 topic 退订,无需手动清理。

显式指定 resource 与 recordId

如果你不在ResourceContext/RecordContext的覆盖范围内(例如在自定义页面、dashboard 或弹窗中),可以显式传入资源名和记录 id:

useSubscribeToRecord(event => { /* ... */ }, 'posts', 123);

这一行等价于订阅resource/posts/123topic。

Tip:如果你的目的仅仅是“保持记录数据最新”,应优先使用useGetOneLive这个实时数据 Hook,而不是自己处理事件后手动refetch,前者会帮你完成订阅、事件处理与数据更新整个闭环(见 docs/useGetOneLive.md)。

参数一览

useSubscribeToRecord的签名如下:

useSubscribeToRecord(callback, resource?, recordId?, options?)
PropRequiredTypeDefaultDescription
callbackRequiredfunction-The callback to execute when an event is received.
resourceOptionalstring-The resource to subscribe to. Defaults to the resource in theResourceContext.
recordIdOptionalstring-The record id to subscribe to. Defaults to the id of the record in theRecordContext.
optionsOptionalobject-The subscription options.

callback:事件处理回调

每当resource/[resource]/[recordId]topic 上有事件发布时,第一个参数传入的回调会被调用,事件对象作为其参数:

const [open, setOpen] = useState(false); const [author, setAuthor] = useState<string | null>(null); const { refetch } = useEditContext(); const { formState: { isDirty }, } = useFormContext(); useSubscribeToRecord((event: Event) => { if (event.type === 'edited') { if (isDirty) { setOpen(true); setAuthor(event.payload.user); } else { refetch(); } } });

useCallback记忆化回调

订阅/退订的开销与回调引用的变化频率直接相关。每次渲染若传入新的内联函数,都会触发一次订阅再退订。原文档建议使用useCallback记忆化回调,把依赖项显式列出,从而避免不必要的订阅/退订抖动:

const [open, setOpen] = useState(false); const [author, setAuthor] = useState<string | null>(null); const { refetch } = useEditContext(); const { formState: { isDirty }, } = useFormContext(); const handleEvent = useCallback( (event: Event) => { if (event.type === 'edited') { if (isDirty) { setOpen(true); setAuthor(event.payload.user); } else { refetch(); } } }, [isDirty, refetch, setOpen, setAuthor] ); useSubscribeToRecord(handleEvent);

回调的第二个参数:unsubscribe

useSubscribe一致,回调函数的第二个参数是一个unsubscribe函数。当需要“收到某个特定事件后就停止监听”时,可以在回调内主动调用它——例如在记录被删除后立刻退订,避免后续再处理该记录的任何事件:

useSubscribeToRecord((event: Event, unsubscribe) => { if (event.type === 'deleted') { // do something unsubscribe(); } if (event.type === 'edited') { if (isDirty) { setOpen(true); setAuthor(event.payload.user); } else { refetch(); } } });

options:订阅行为控制

options对象支持以下属性,用于精细控制订阅/退订行为:

  • enabled:是否订阅,默认true。设为false可延迟订阅(例如在异步拿到 record id 之前先不订阅);
  • once:收到第一个事件后是否自动退订,默认false。适合“等待某个一次性事件”(如任务完成通知);
  • unsubscribeOnUnmount:组件卸载时是否退订,默认true

这些选项与useSubscribe完全相同,更多细节与示例(如once: true的一次性订阅演示)参见 docs/useSubscribe.md。

recordId:覆盖订阅的记录 id

默认情况下,useSubscribeToRecord使用RecordContext中记录的 id 来构建订阅 topic。你也可以通过第三个参数显式覆盖:

// 将订阅 'resource/posts/123' topic useSubscribeToRecord(event => { /* ... */ }, 'posts', 123);

一个值得注意的边界情况:如果传入 null 作为 record id,Hook 不会订阅任何 topic。这在你尚未确定记录 id(例如列表页等待选中行)时非常有用,可以安全地占位。

resource:覆盖订阅的资源名

同理,默认使用ResourceContext中的资源名来构建 topic,可通过第二个参数显式覆盖:

// 将订阅 'resource/posts/123' topic useSubscribeToRecord(event => { /* ... */ }, 'posts', 123);

另一个边界情况:如果传入空字符串作为资源名,Hook 同样不会订阅任何 topic

底层原理:topic 与事件格式

useSubscribeToRecord之所以能“猜”出订阅目标,是因为ra-realtime对 CRUD 场景有一套约定的 topic 命名规范(详见 docs/RealtimeDataProvider.md 的 "Topic And Event Format" 一节):

  • 记录级 topic:resource/[resource]/[id]—— 正是useSubscribeToRecord订阅的;
  • 列表级 topic:resource/[resource]—— 由useSubscribeToRecordList订阅(见 docs/useSubscribeToRecordList.md)。

事件对象是带typepayload两个字段的普通 JavaScript 对象。对于 CRUD 操作,ra-realtime约定使用createdupdateddeleted三种事件类型,例如记录更新时后端会同时向两个 topic 发布事件:

{ topic: `resource/${resource}/id`, event: { type: 'updated', payload: { ids: [id] }, }, } { topic: `resource/${resource}`, event: { type: 'updated', payload: { ids: [id] }, }, }

记录创建时仅向列表 topic 发布created事件,删除时则同时向记录 topic 与列表 topic 发布deleted事件(事件格式详见 docs/RealtimeDataProvider.md 的 "CRUD Events" 一节)。理解了这套约定,你就能在自己的回调中针对event.type精准分流处理。

与其他 Hook 的选型对比

ra-realtime的 Hook 家族中(docs/Realtime.md 有完整列表),useSubscribeToRecord的定位与取舍如下:

Hook订阅 topic适用场景
useSubscribe任意 topic(字符串)通用消息、聊天频道、自定义 topic(见 docs/useSubscribe.md)
useSubscribeToRecordresource/[resource]/[id]关注单条记录的事件,如并发编辑冲突提示
useSubscribeToRecordListresource/[resource]关注整个资源列表的事件,如"有人新建/更新/删除了记录"的通知与刷新(见 docs/useSubscribeToRecordList.md)
useGetOneLive内部封装只想让单条记录数据保持最新,不需要自己处理事件

选型建议:如果只需要监听单个记录的变更并联动 UI(弹窗、通知、局部刷新),useSubscribeToRecord是最直接的选择;如果数据只是“要最新”,优先useGetOneLive;如果是列表场景,则看useSubscribeToRecordList<ListLiveUpdate>组件。

小结

useSubscribeToRecord以最小的心智负担把“单记录实时事件监听”接入 react-admin:无需手写 topic、无需手动订阅/退订,仅凭ResourceContextRecordContext即可完成全部推断;配合useCallback记忆化、unsubscribe第二参数以及enabled/once/unsubscribeOnUnmount选项,可以构建出从冲突提示、实时通知到一次性事件等待等丰富的协作型交互。掌握它的关键在于理解底层的 topic 约定(resource/[resource]/[id])与 CRUD 事件格式(created/updated/deleted),两者共同构成了 react-admin 实时协作体系的基石。更多配套能力(实时数据 Hook、锁机制、菜单徽标等)可继续查阅 docs/Realtime.md 与 docs/RealtimeDataProvider.md。

【免费下载链接】react-adminA frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design项目地址: https://gitcode.com/gh_mirrors/re/react-admin

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询