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 的用法、参数、回调语义,以及它与useSubscribe、useSubscribeToRecordList、useGetOneLive的取舍,帮助你在协同编辑、并发冲突提示、实时通知等场景中落地实时能力。
背景: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就是其中之一,与其并列的还有useSubscribe、useSubscribeCallback、useSubscribeToRecordList、usePublish(完整列表见 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会分别从ResourceContext和RecordContext读取当前的资源名与记录 id。上面的例子中,当应用收到resource/books/123topic 上的事件时,会触发通知——其中books来自ResourceContext,123来自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?)| Prop | Required | Type | Default | Description |
|---|---|---|---|---|
callback | Required | function | - | The callback to execute when an event is received. |
resource | Optional | string | - | The resource to subscribe to. Defaults to the resource in theResourceContext. |
recordId | Optional | string | - | The record id to subscribe to. Defaults to the id of the record in theRecordContext. |
options | Optional | object | - | 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)。
事件对象是带type和payload两个字段的普通 JavaScript 对象。对于 CRUD 操作,ra-realtime约定使用created、updated、deleted三种事件类型,例如记录更新时后端会同时向两个 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) |
useSubscribeToRecord | resource/[resource]/[id] | 关注单条记录的事件,如并发编辑冲突提示 |
useSubscribeToRecordList | resource/[resource] | 关注整个资源列表的事件,如"有人新建/更新/删除了记录"的通知与刷新(见 docs/useSubscribeToRecordList.md) |
useGetOneLive | 内部封装 | 只想让单条记录数据保持最新,不需要自己处理事件 |
选型建议:如果只需要监听单个记录的变更并联动 UI(弹窗、通知、局部刷新),useSubscribeToRecord是最直接的选择;如果数据只是“要最新”,优先useGetOneLive;如果是列表场景,则看useSubscribeToRecordList或<ListLiveUpdate>组件。
小结
useSubscribeToRecord以最小的心智负担把“单记录实时事件监听”接入 react-admin:无需手写 topic、无需手动订阅/退订,仅凭ResourceContext与RecordContext即可完成全部推断;配合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),仅供参考