- 前端
- UI组件
【免费下载链接】react-admin
A frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design
<WithLocks>是 react-admin 实时协作(realtime / locks)能力中的一个声明式容器组件:它在挂载时调用dataProvider.getLocks()拉取当前资源的全部锁,将结果放入LocksContext,并订阅该资源的锁主题,收到新事件后自动重新获取锁,从而让锁状态在多个用户之间实时保持同步。本文基于本仓库的 WithLocks 文档,结合 Realtime 文档、Realtime Data Provider 文档 与配套 Hook 文档,从工作原理、安装配置、完整用法到数据结构逐层展开,读完即可在列表、详情、编辑等场景中正确使用锁信息来防止并发编辑冲突。
为什么需要<WithLocks>:协作场景下的"锁"问题
当多个用户并行处理同一批数据时(例如客服团队共同处理工单),会出现两类典型问题:
- 数据覆盖:两人同时编辑同一条记录,后保存者覆盖先保存者的修改;
- 信息滞后:用户看到的界面无法及时反映"这条记录此刻正被谁编辑"。
react-admin 的实时特性(ra-realtime,Enterprise Edition 的一部分)通过锁(Lock)机制解决这些问题。正如 Realtime 文档 所描述的:
用户可以主动请求锁,或通过编辑资源获得锁。当资源被锁定时,其他用户无法编辑它;锁释放后,其他用户才能再次编辑。
锁机制依赖 data provider 提供lock、unlock、getLock、getLocks四个方法(详见下文"data provider 要求")。而<WithLocks>正是把"获取一组锁并保持实时更新"这件事封装成了开箱即用的组件,让你无需自己管理订阅生命周期,就能在任意位置读取当前资源的所有锁。
工作原理:挂载即取锁,订阅即刷新
根据 WithLocks 文档,<WithLocks>的行为可以用三步概括:
- 挂载时取锁:调用
dataProvider.getLocks()获取当前资源的所有锁; - 订阅锁主题:订阅当前资源对应的 locks 主题;
- 事件驱动刷新:每当收到新事件,就重新获取一次锁,保证
LocksContext中的数据始终是最新的。
其中 "locks 主题" 与数据提供者的事件约定相关。在 RealtimeDataProvider.md 的 Topic 与事件格式 一节中说明:ra-realtime的 CRUD 组件订阅resource/[name]和resource/[name]/[id]形式的主题,事件是包含type与payload字段的 JavaScript 对象,例如{ type: 'created', payload: { ids: [id] } }。锁数据的实时刷新正是建立在这套 Pub/Sub 机制之上:后端(或服务端业务逻辑)在锁被创建/释放时向对应主题发布事件,<WithLocks>收到后重新拉取锁列表。
注意:
<WithLocks>是 Enterprise Edition 组件,托管在私有 npm registry 中,使用需要有效的 Enterprise Edition 订阅(见 WithLocks 文档)。在本仓库的 Open Source 代码中搜索不到getLocks的 ra-core 实现,符合该能力由闭源@react-admin/ra-core-ee包提供的定位。
安装与前提条件
安装
<WithLocks>由@react-admin/ra-core-ee包导出,按 WithLocks 文档 的说明安装:
npm install --save @react-admin/ra-core-ee # 或者使用 yarn yarn add @react-admin/ra-core-ee更完整的实时特性(包括实时列表、实时通知、锁的各类 Hook)由@react-admin/ra-realtime包提供,安装方式一致:
npm install --save @react-admin/ra-realtime # or yarn add @react-admin/ra-realtime这两个包都属于 React-Admin Enterprise Edition,托管在私有 npm registry,需要订阅 Enterprise Edition 计划才能安装(见 Realtime 文档的安装章节)。
data provider 要求:必须实现锁方法
<WithLocks>依赖dataProvider.getLocks()。要支持锁特性,data provider 除了 CRUD 方法外,还必须额外实现 4 个锁方法(DataProviderLive.md 与 RealtimeDataProvider.md 均有记载):
dataProvider.lock(resource, { id, identity, meta })dataProvider.unlock(resource, { id, identity, meta })dataProvider.getLock(resource, { id, meta })dataProvider.getLocks(resource, { meta })
其中getLocks用于获取某个资源的全部锁,正是<WithLocks>在挂载时调用的方法。这些方法应返回在实时总线确认操作后 resolve 的 Promise。
若你的后端是 Supabase、API Platform 或 Mercure,ra-realtime提供addRealTimeMethodsBasedOnSupabase、addRealTimeMethodsBasedOnApiPlatform、addRealTimeMethodsBasedOnMercure等增强函数,可以直接给普通 data provider 附加实时与锁能力;其他传输方式(WebSocket、长轮询、GraphQL Subscription 等)则需要自己实现这些方法(参考 RealtimeDataProvider.md 的 Custom Adapter 示例)。
另外,ra-realtime还提供addLocksMethodsBasedOnALockResource,将锁操作映射到普通 CRUD:getLocks变为getList('locks'),lock变为create('locks'),适合用一张独立的locks表来存储锁(见 RealtimeDataProvider.md 的 Locks Based On A Lock Resource 一节)。
基本用法:把<WithLocks>包在列表外面
WithLocks 文档 给出的核心用法是:用<WithLocks>包裹整个列表,然后在列表内部通过useLocksContext()读取锁,并根据锁状态自定义渲染。
import { ListBase, useRecordContext } from 'ra-core'; import { WithLocks, useLocksContext } from '@react-admin/ra-core-ee'; import { DataTable } from 'your-ra-ui-library'; const LockField = () => { const locks = useLocksContext(); const record = useRecordContext(); if (!record) return null; const lock = locks.find(lock => lock.recordId === record?.id); if (!lock) return null; return <span>Locked by {lock.identity}</span>; }; const PostList = () => ( <WithLocks> <ListBase> <DataTable> {/* 其他列 */} <DataTable.Col source="lockStatus" field={LockField} /> </DataTable> </ListBase> </WithLocks> );要点拆解:
useLocksContext():读取<WithLocks>提供的锁数组(Lock[])。调用位置必须在<WithLocks>的子树内;useRecordContext():获取当前行(记录),lock.recordId === record.id用于判断当前记录是否被锁定;lock.identity:加锁者的身份标识,可以直接展示在界面上(例如"Locked by julien");- 锁不存在时返回
null:这样未锁定的记录不会渲染任何锁提示,界面保持干净。
Props 一览
<WithLocks>的 props 非常精简,只有一项(WithLocks 文档):
| Prop | Required | Type | Default | Description |
|---|---|---|---|---|
children | Required | ReactNode | 在LocksContext内部渲染的组件 |
也就是说,你只需关心"把哪些内容包进锁上下文",锁的获取、订阅与刷新全部由组件内部完成。
LocksContext 中的数据形态:Lock 对象长什么样
useLocksContext()返回的是一个Lock[]数组。单个锁对象的标准格式在 RealtimeDataProvider.md 的 Lock Format 一节 中有明确定义:
{ resource: 'posts', recordId: 123, identity: 'julien', createdAt: '2023-01-02T21:36:35.133Z', }字段说明:
resource:资源名(如'posts');recordId:被锁定记录的 id(如123);identity:加锁者身份,字符串或数字均可,例如用户名、id 或认证 token;createdAt:锁的获取时间(ISO 时间字符串)。
dataProvider.getLock()与dataProvider.getLocks()应返回符合上述结构的锁对象/锁数组。而加锁、解锁方法(lock、unlock)的参数约定为:
resource:资源名(如'posts');params对象,包含:id:记录 id(如123);identity:加锁者标识(字符串或数字,例如'julien',也可以是认证 token);meta:透传给 data provider 的附加信息(可选)。
如果使用addLocksMethodsBasedOnALockResource的locks资源方案,后端锁记录还需要id字段,完整结构参见 RealtimeDataProvider.md:
{ "id": 123, "identity": "Toad", "resource": "people", "recordId": 18, "createdAt": "2020-09-29 10:20" }进阶实战一:在数据表格中禁用编辑与删除按钮
<WithLocks>最常见的应用场景是"锁定的记录不允许他人操作"。把锁数据从上下文中取出后,可以像 useGetLocks 文档 中的示例那样,将编辑、删除按钮置灰:
const MyPostGrid = () => { const resource = useResourceContext(); const { data: locks } = useGetLocks(resource); return ( <DataTable bulkActionButtons={false}> <DataTable.Col label="Title"> <MyPostTitle locks={locks} /> </DataTable.Col> <DataTable.Col label="Actions" align="right"> <MyPostActions locks={locks} /> </DataTable.Col> </DataTable> ); }; const MyPostTitle = ({ locks }: { locks: Lock[] }) => { const record = useRecordContext(); const lock = locks.find(l => l.recordId === record.id); return ( <> <TextField source="title" /> {lock && ( <span style={{ color: 'red' }}> {` (Locked by ${lock.identity})`} </span> )} </> ); }; const MyPostActions = ({ locks }: { locks: Lock[] }) => { const record = useRecordContext(); const locked = locks.find(l => l.recordId === record.id); return ( <> <DeleteButton disabled={!!locked} /> <LockableEditButton disabled={!!locked} /> </> ); };这套逻辑与<WithLocks>的用法完全同构——差别只在于数据来源是useLocksContext()还是useGetLocks()。若希望"取锁 + 实时订阅 + 重新拉取"由组件统一管理,就用<WithLocks>;若需要在更细粒度处单独控制数据流,则使用useGetLocks/useGetLocksLive等 Hook(详见下文"配套的锁 Hook 家族")。
进阶实战二:列表展示锁状态 + 详情页自动加锁
<WithLocks>负责"读"锁,而锁的"写"(加锁/解锁)由配套 Hook 完成。一个完整的防并发编辑方案通常这样组合:
1. 列表页:用<WithLocks>实时展示每条记录的锁定状态(如上面的LockField),让用户一眼看出"这条正被谁编辑"。
2. 编辑页:用<LockOnMount>(useLockOnMount的组件形态)在挂载时锁定当前记录、卸载时自动解锁:
import { Edit, SimpleForm, TextInput } from 'react-admin'; import { LockOnMount } from '@react-admin/ra-realtime'; const PostEdit = () => ( <Edit> <SimpleForm> <TextInput source="title" fullWidth /> <TextInput source="headline" fullWidth multiline /> <TextInput source="author" fullWidth /> <LockOnMount /> </SimpleForm> </Edit> );<LockOnMount>会通过authProvider.getIdentity()获取当前用户身份,并自动从上下文(或路由)猜测resource与recordId(见 LockOnMount 文档)。注意:如果用户带着已锁定的记录直接关闭标签页/浏览器,LockOnMount会阻止导航并显示通知,直到记录被解锁。
3. 交互式加锁:也可以让用户"获得焦点时加锁",Realtime 文档 的锁示例展示了useGetLockLive+useLockOnCall的用法——先查当前锁,再判断表单是否禁用,最后在onFocus时请求加锁:
const { data: lock } = useGetLockLive('tickets', { id: record.id }); const { identity } = useGetIdentity(); const isFormDisabled = lock && lock.identity !== identity?.id; const [doLock] = useLockOnCall({ resource: 'tickets' }); // TextInput 的 onFocus 里调用 doLock(),并对输入控件传入 disabled={isFormDisabled}这样,列表页(<WithLocks>只读视角)、编辑页(<LockOnMount>自动加锁)、表单(useLockOnCall聚焦加锁)三处协同,即可构成完整的协作编辑闭环。
配套的锁 Hook 家族
<WithLocks>是锁能力的一个组件入口,react-admin 实时体系中还有一整套锁相关 Hook(见 Realtime 文档 与 DataProviderLive.md),按职责可分为:
| 类别 | API | 作用 |
|---|---|---|
| 读取单条锁 | useGetLock | 调用dataProvider.getLock()获取某条记录的锁 |
| 读取单条锁(实时) | useGetLockLive | 获取单条锁并订阅实时更新 |
| 读取全部锁 | useGetLocks | 调用dataProvider.getLocks()获取资源全部锁 |
| 读取全部锁(实时) | useGetLocksLive | 获取全部锁并订阅实时更新 |
| 加锁 / 解锁 | useLock、useUnlock | 手动加锁、解锁 |
| 自动加锁 | useLockOnMount、useLockOnCall | 挂载时加锁 / 调用时加锁 |
选择建议:
- 只需要"在列表里展示锁状态并实时刷新" → 用
<WithLocks>(声明式、最省心); - 需要在自定义逻辑中单独管理数据流(例如在列表外部取锁) → 用
useGetLocks/useGetLocksLive; - 需要锁定/解锁动作(编辑页、聚焦加锁) → 用
useLock/useUnlock/useLockOnMount/useLockOnCall。
常见问题与注意事项
- 必须是 Enterprise Edition:
<WithLocks>由@react-admin/ra-core-ee提供,私有 registry 托管,需要订阅 Enterprise Edition 计划才能安装使用(WithLocks 文档)。 - data provider 必须实现锁方法:
<WithLocks>依赖getLocks,若 data provider 未实现会直接报错。建议先用addRealTimeMethodsBasedOn*系列增强函数,或按 RealtimeDataProvider.md 的 Custom Adapter 示例手写subscribe/unsubscribe/publish与 4 个锁方法。 - 锁的实时性依赖后端发布事件:
<WithLocks>靠订阅锁主题 + 收到事件后重新getLocks()来刷新。如果后端没有在锁创建/释放时向对应主题发布事件,界面锁状态不会自动更新(主题与事件格式见 RealtimeDataProvider.md)。 - 身份标识要稳定:锁对象中的
identity与authProvider.getIdentity()返回的 id 需要能对上(例如都是用户 id 或用户名),否则"判断是否本人持有的锁"会失效。 - 锁数据只读:
useLocksContext()提供的是锁的只读视图;加锁、解锁请走useLock/useUnlock/useLockOnMount/useLockOnCall,不要直接修改锁数组。
小结
<WithLocks>把"取锁—订阅—刷新"这一套复杂的实时流程收敛成一个容器组件:挂载即调用dataProvider.getLocks(),订阅当前资源的锁主题,事件到达后自动重新拉取,并将最新的Lock[]暴露给LocksContext。在列表、看板、数据表格中,配合useLocksContext()即可快速实现"展示谁锁定了哪条记录""锁定记录禁用编辑删除"等协作功能;再与useGetLockLive、useLockOnMount、useLockOnCall等 Hook 组合,就能搭建出完整的防并发编辑方案。
相关文档与源码入口:
- WithLocks 文档
- Realtime 概览(Locks 章节)
- Realtime Data Provider(锁方法签名、Lock 格式、locks 资源方案)
- DataProviderLive.md(实时与锁的数据提供者要求)
- useGetLocks Hook 文档
- LockOnMount 组件文档
- useLockOnMount Hook 文档
- 前端
- UI组件
【免费下载链接】react-admin
A frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design
相关推荐
react-admin 实时协作之 useLockCallbacks:锁定/解锁回调与记录锁状态管理的完整指南
react admin 实时协作之 useLockCallbacks:锁定/解锁回调与记录锁状态管理的完整指南 useLockCallbacks 是 react
前端UI组件react-admin 记录锁状态查询:`useGetLock` 钩子深度指南
react admin 记录锁状态查询: useGetLock 钩子深度指南 useGetLock 是 react admin 企业版 ra realtime
前端UI组件react-admin 实时锁机制:`useLockOnCall` 手动触发记录锁 Hook 完整实战指南
react admin 实时锁机制: useLockOnCall 手动触发记录锁 Hook 完整实战指南 useLockOnCall 是 react admin
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考