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
导读
draggableId与droppableId是 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 类型形式声明了Id、TypeId、DroppableId、DraggableId四个别名,它们共同参与构建DraggableLocation、Combine、DragStart、DropResult等公开类型。因此你在onDragEnd等回调中拿到的result.draggableId、result.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 />。具体包含两层含义:
- 跨列表唯一:即使你有多个相互连接的列表,每个
<Droppable />的 id、每个<Draggable />的 id 都必须唯一,不能因为项目位于不同列表而重复使用同一个 id。 - 跨 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。库在启动拖拽时会通过findDraggable、findDragHandle等工具函数(见 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),你随时可以更改draggableId或droppableId,包括重排之后。但为了避免异常,必须避免在两个组件之间复用 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),仅供参考