news 2026/9/19 8:21:32

react-beautiful-dnd 标识符(Identifiers)完全指南:draggableId 与 droppableId 的规则、约束与内部实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
react-beautiful-dnd 标识符(Identifiers)完全指南:draggableId 与 droppableId 的规则、约束与内部实现

react-beautiful-dnd 标识符(Identifiers)完全指南:draggableId 与 droppableId 的规则、约束与内部实现

【免费下载链接】react-beautiful-dndBeautiful and accessible drag and drop for lists with React项目地址: https://gitcode.com/gh_mirrors/re/react-beautiful-dnd

导读

draggableIddroppableId是 react-beautiful-dnd 中<Draggable /><Droppable />的身份凭证,也是整个拖拽引擎做维度收集、命中判定与状态更新的核心索引。本文以官方文档 identifiers.md 为主体,结合仓库源码(注册表、校验逻辑、DOM 属性注入)深入讲解 id 的字符串约束、全局唯一性要求、"不要用索引当 id" 的底层原因,以及为什么这些规则是出于简化与性能的设计取舍。读完本文,你将能写出稳定、无异常的拖拽列表 id 方案。

一、什么是 draggableId 与 droppableId

在 react-beautiful-dnd 中,每个<Draggable /><Droppable />都必须提供一个 id:

  • <Draggable />draggableId:标识一个可被拖拽的项目;
  • <Droppable />droppableId:标识一个可以接收拖拽的容器(列表)。

这两个 id 分别作为对应组件的必填 prop 传入,例如:

import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; <DragDropContext onDragEnd={onDragEnd}> <Droppable droppableId="list-1"> {(provided) => ( <ul ref={provided.innerRef} {...provided.droppableProps}> {items.map((item, index) => ( <Draggable key={item.id} draggableId={item.id} index={index}> {(dragProvided) => ( <li ref={dragProvided.innerRef} {...dragProvided.draggableProps}> {item.content} </li> )} </Draggable> ))} {provided.placeholder} </ul> )} </Droppable> </DragDropContext>

在类型层面,id 被定义为字符串类型,src/types.js 中给出了公开类型别名:

export type Id = string; export type DraggableId = Id; export type DroppableId = Id;

官方 types 指南 中同样以 Flow 类型形式声明了IdTypeIdDroppableIdDraggableId四个别名,它们共同参与构建DraggableLocationCombineDragStartDropResult等公开类型。因此你在onDragEnd等回调中拿到的result.draggableIdresult.source.droppableId等字段,全部都是字符串 id。

二、id 必须是字符串(String)

官方文档明确规定:期望 id 是一个string。这一要求不仅停留在类型层面,还在运行时强制校验——如果传入非字符串 id,react-beautiful-dnd 会直接抛出异常(throw an error)。

  • 在 Draggable 的 prop 校验 中:
const id = props.draggableId; invariant(id, 'Draggable requires a draggableId'); invariant( typeof id === 'string', `Draggable requires a [string] draggableId. Provided: [type: ${typeof id}] (value: ${id})`, );
  • 在 Droppable 的 prop 校验 中:
invariant(props.droppableId, 'A Droppable requires a droppableId prop'); invariant( typeof props.droppableId === 'string', `A Droppable requires a [string] droppableId. Provided: [${typeof props.droppableId}]`, );

这两段校验都运行在开发模式的 setup warning 阶段(useDevSetupWarning),也就是说你在开发环境就能立刻发现 id 类型错误,而不是等到拖拽行为异常才排查。不要使用数字或其他类型的值作为 id,即使 JavaScript 的对象键会自动把数字强制转成字符串,也应遵守规范显式传入字符串。

三、id 必须在 DragDropContext 内全局唯一

文档强调:一个 id 必须在同一个<DragDropContext />内唯一地标识一个<Draggable /><Droppable />。具体包含两层含义:

  1. 跨列表唯一:即使你有多个相互连接的列表,每个<Droppable />的 id、每个<Draggable />的 id 都必须唯一,不能因为项目位于不同列表而重复使用同一个 id。
  2. 跨 type 唯一:即使两个<Droppable />typeprop 不同,它们的droppableId也不能相同。

这一要求可以从注册表(registry)的实现得到印证。react-beautiful-dnd 使用 create-registry.js 维护所有组件条目,其内部数据结构是以 id 为键的对象映射

const entries: EntryMap = { draggables: {}, // { [draggableId]: DraggableEntry } droppables: {}, // { [droppableId]: DroppableEntry } };

register操作本质就是一次对象赋值:

register: (entry: DraggableEntry) => { entries.draggables[entry.descriptor.id] = entry; notify({ type: 'ADDITION', value: entry }); },

同理,droppableAPI.register执行entries.droppables[entry.descriptor.id] = entry。在这种以 id 为键的映射结构下,重复的 id 会直接互相覆盖,导致先注册的组件被后注册的组件顶掉,从而引发"找不到条目""拖拽维度错误"等连锁问题。所以文档才会强调唯一性——这不是审美要求,而是数据结构决定的硬性约束。

此外,registry-types.js 显示每个注册条目由uniqueId(组件内部自增的唯一编号)与descriptor(描述符,含 id)组成,用于区分"同一个组件更新"与"另一个组件复用了 id"。

四、id 如何进入 DOM:data 属性透传

id 不仅仅存在于 React 状态中,它还会被写入真实 DOM 节点,供库内部通过 DOM 查询定位组件。以 Draggable 的实现 为例,draggableProps会携带:

draggableProps: { 'data-rbd-draggable-context-id': contextId, 'data-rbd-draggable-id': draggableId, style, onTransitionEnd, },

而 Droppable 的实现 同样会注入:

'data-rbd-droppable-id': droppableId,

对应的类型声明可见 draggable-types.js 与 droppable-types.js。库在启动拖拽时会通过findDraggablefindDragHandle等工具函数(见 get-elements)依据这些 data 属性定位真实的拖拽节点。这意味着id 必须能被安全地放进 HTML 属性,这也是要求 id 为字符串、且避免使用容易产生特殊字符的值的原因之一。

五、避免复用 id:为什么不要用 index 当 id

官方文档给出了一条最重要的实战建议:不要基于 index 构造 draggableId 或 droppableId

Don't base an id on a index

最佳实践是:把一个 id 与一条数据(data)关联起来,在重排(reorder)之间不更新它。例如上面示例中的draggableId={item.id},其中item.id是数据本身的稳定主键。

当然,官方也说明:除了拖拽进行中(during a drag),你随时可以更改draggableIddroppableId,包括重排之后。但为了避免异常,必须避免在两个组件之间复用 id——而基于 index 生成 id 正是触发这种"复用"的典型场景。

5.1 内部发生了什么:以 droppableId 变更为例

文档用一个三步示例展示了内部引用变更过程,这里完整保留并加以解释:

步骤 1:更新 Droppable
  • 旧 droppableId:"droppable-0"
  • 新 droppableId:"droppable-1"
  • 👉 删除对"droppable-0"的引用
  • 👉 添加对"droppable-1"的引用
步骤 2:更新 Droppable(隐患出现)
  • 旧 droppableId:"droppable-1"😢(这正是上一步刚注册的新 id!)
  • 新 droppableId:"droppable-2"
  • 👉 删除对"droppable-1"的引用 😢(会误删掉我们刚注册的"droppable-1"
  • 👉 添加对"droppable-2"的引用
步骤 3:更新 Droppable(异常爆发)
  • 旧 droppableId:"droppable-1"💥(该引用已在步骤 2 被删除)
  • 新 droppableId:"droppable-5"
  • 👉 删除对"droppable-1"的引用 💥(因"droppable-1"已不存在而抛出异常)

5.2 源码印证:unregister 的删除逻辑

上述"删除引用"在源码中对应注册表的unregister操作。create-registry.js 中 Draggable 的注销逻辑为:

unregister: (entry: DraggableEntry) => { const draggableId: DraggableId = entry.descriptor.id; const current = findDraggableById(draggableId); // 可能已被 clean 提前移除 if (!current) { return; } // uniqueId 不匹配说明是过期的注册记录 if (entry.uniqueId !== current.uniqueId) { return; } delete entries.draggables[draggableId]; notify({ type: 'REMOVAL', value: entry }); },

delete entries.draggables[draggableId]正是"删除引用"。当一个基于 index 的 id 从 1 变到 2 时,组件卸载流程会把"droppable-1"删掉;而此时新的、恰好也叫"droppable-1"的组件可能刚刚注册——旧组件的注销会误删新组件的引用,最终在第三步出现Cannot find droppable entry with id之类的 invariant 异常(见 getDroppableById):

invariant(entry, `Cannot find droppable entry with id [${id}]`);

同理,Draggable 的getDraggableById也会在找不到条目时抛出Cannot find draggable entry with id [...]

5.3 拖拽期间的额外约束

值得补充的是,虽然非拖拽期间可以改 id,但拖拽期间修改 id 是禁止的。从 dimension-marshal.js 的shouldPublishUpdate可以看到,库会对拖拽中新增/移除的 Draggable 进行严格检查:除非是虚拟列表(virtual mode),否则会给出警告并拒绝发布这些变更。因此把 id 与数据强绑定、保持 id 稳定,是规避一切异常的最省心方案。

六、这些规则可以改变吗?

文档明确表示:可以。作者承认这些约束并非绝对必要,而是"为了简化与性能"(for simplicity and performance)做出的取舍:

  • 简化:以 id 为键的对象映射(entries.draggables[id]entries.droppables[id])让注册、查询、注销都变成 O(1) 的常数时间操作,代码路径清晰直观;
  • 性能:拖拽过程中(尤其是 dimension-marshal 与 while-dragging-publisher)会频繁按 id 查找条目与比对描述符,扁平映射比遍历数组高效得多。

如果你对这套规则有强烈意见,官方欢迎在 GitHub issue 中继续讨论。不过在实践中,绝大多数应用采用"id = 数据主键"的模式即可完美规避所有限制,无需触及这一层设计决策。

七、最佳实践速查

综合文档与源码,给出如下 checklist:

规则说明违反后果
id 必须是字符串传入非字符串会触发 invariant 异常(draggable 校验、droppable 校验)开发环境直接抛错
同一 DragDropContext 内全局唯一包括跨列表、跨type(注册表以 id 为键,重复会覆盖,见 create-registry.js)条目互相覆盖、找不到条目
不要基于 index 构造 id重排会改变 index,导致 id 迁移并误删新引用Cannot find ... entry with id异常
id 与数据绑定,重排不更新draggableId={item.id}是最稳妥写法状态错乱、异常
拖拽期间不要改 id拖拽中变更会被 dimension-marshal 拒绝(虚拟列表除外,见 dimension-marshal.js)警告或被忽略
注意 id 会写入 DOM data 属性见 draggable.jsx 与 droppable.jsx 的data-rbd-*-id影响内部 DOM 查询定位

一句话总结:把 id 当作数据的主键来对待——稳定、唯一、始终是字符串,你的拖拽列表就不会在 id 上踩坑。

【免费下载链接】react-beautiful-dndBeautiful and accessible drag and drop for lists with React项目地址: https://gitcode.com/gh_mirrors/re/react-beautiful-dnd

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 8:18:44

CSS cursor 完全指南:取值体系、踩坑与自定义光标实战

简介&#xff1a;CSS cursor&#xff08;鼠标样式&#xff09;是前端开发中非常实用的属性&#xff0c;这份独立 PDF 将 cursor 的常用可选值集中整理成一份速查笔记&#xff0c;面向网页设计、前端开发入门者&#xff0c;也适合需要快速确认光标交互反馈的开发者。内容按用途分…

作者头像 李华
网站建设 2026/9/19 8:18:04

对公客户风险限额试点培训:从敞口计算到SQL实现与验证

简介&#xff1a;面向银行信贷风险管理和对公客户经理的培训资料&#xff0c;围绕对公客户风险限额试点展开&#xff0c;系统讲解现行限额设定框架的局限、新限额方案的总体设计&#xff0c;以及公司类、事业类、金融机构、新成立客户、集团客户等不同类别限额计算方法和调整步…

作者头像 李华
网站建设 2026/9/19 8:14:54

DJI Pocket 4P固件升级,FrameTap远程拍摄更好用了

DJI最新的Osmo Pocket 4P固件更新&#xff0c;重点不在于新增一个吸睛的拍摄模式&#xff0c;而是让这款小巧的双镜头相机在真实创作场景中变得更易用。其中一款配件表现尤为亮眼&#xff1a;Osmo FrameTap。2026年9月的更新&#xff0c;固件版本号为01.01.71.31&#xff0c;为…

作者头像 李华
网站建设 2026/9/19 8:14:26

人机交互实验数据采集的三大刚性约束

1. 为什么“人机交互实验场景”是具身智能数据采集的真正分水岭很多人一听到“具身智能数据采集”&#xff0c;第一反应是堆传感器、铺摄像头、买机械臂——硬件清单列得比菜市场采购单还全。但我在三年内参与过7个高校实验室和3家机器人初创公司的数据采集系统搭建&#xff0c…

作者头像 李华
网站建设 2026/9/19 8:13:19

平均值、标准差与变异系数:Excel统计分析与数据波动解读

这三个指标是数据分析里最基础、也最常用的一组统计量&#xff1a;平均值描述数据的集中趋势&#xff0c;标准差描述数据的离散程度&#xff0c;变异系数则用来比较不同量纲或量级数据的波动性。在Excel里&#xff0c;它们分别对应AVERAGE、STDEV&#xff08;或STDEV.S/STDEV.P…

作者头像 李华