react-beautiful-dnd innerRef 完全指南:让 Draggable 与 Droppable 正确拿到 HTMLElement
2026/9/19 4:16:22 网站建设 项目流程

react-beautiful-dnd innerRef 完全指南:让 Draggable 与 Droppable 正确拿到 HTMLElement

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

innerRef是 react-beautiful-dnd 中<Draggable /><Droppable />暴露给使用者的关键回调属性,它负责把组件的真实 DOM 节点交给库引擎。本文以官方指南为主线,结合仓库源码与测试用例,系统讲解innerRef的用法、常见误区、自定义组件透传方案以及底层校验机制,读完即可在自己的项目中正确接入拖拽能力并规避“应用爆炸”式的 ref 陷阱。

为什么DraggableDroppable需要innerRef

react-beautiful-dnd 是一个基于 DOM 测量的拖拽库:在拖拽开始前,它需要通过getBoundingClientRect()读取被拖拽项与可放置容器的位置、尺寸,并据此计算位移与排序。因此,<Draggable /><Droppable />必须拿到一个真实的HTMLElement,而这个节点正是通过DraggableProvidedDroppableProvided对象上的innerRef属性提供的。

如果你还不熟悉 React 的 ref 机制,建议先阅读 React 官方文档中的 Refs and the DOM 章节,理解 ref 回调的触发时机与节点生命周期后再继续。

基本用法:将innerRef挂到你的 DOM 节点上

最简单、最标准的用法,是把provided.innerRef直接作为ref传给渲染函数返回的 JSX 元素(ReactElement):

<Draggable draggableId="draggable-1" index={0}> {(provided, snapshot) => ( <div + ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} > <h4>My draggable</h4> </div> )} </Draggable>;
<Droppable droppableId="droppable-1"> {(provided, snapshot) => ( <div + ref={provided.innerRef} {...provided.droppableProps} > <h2>I am a droppable!</h2> {provided.placeholder} </div> )} </Droppable>;

从源码可以看到,innerRef实际就是组件内部维护的一个 ref 写入函数。在 src/view/draggable/draggable.jsx 中,Draggable通过useRef保存节点并用useCallback定义了setRef;随后在provided对象里暴露innerRef: setRef(见 draggable.jsx)。Droppable同样以setDroppableRef作为provided.innerRef(见 src/view/droppable/droppable.jsx 与 droppable.jsx)。

当 ref 被正确写入后,getRef返回的节点会通过useDraggablePublisher注册进 registry,并在每次拖拽开始时用于计算尺寸(见 src/view/use-draggable-publisher/use-draggable-publisher.js 与 src/view/use-draggable-publisher/get-dimension.js)——尺寸计算依赖getBoundingClientRect()getComputedStyle()等标准 DOM API,这也就解释了为什么节点必须是HTMLElement而不是任意对象。

并不是所有的 ref 都“生而平等”

很多困惑源于 React 中ref回调在不同目标上的返回值差异:

  • 当 ref 挂在组件(Component)上,如<Person ref={...} />,回调收到的是Person组件的实例
  • 当 ref 挂在ReactElement上,如<div ref={...} />,回调收到的是该元素对应的HTMLElement

下面的示例直观展示了这两种差异:

class Person extends React.Component { state = { sayHello: false, }; sayHello() { this.setState({ sayHello: true, }); } render() { if (this.state.sayHello) { return <div {...this.props}>Hello</div>; } return <div {...this.props}>'I am a person, I think..'</div>; } } class App extends React.Component { setPersonRef = (ref) => { this.personRef = ref; // 当 ref 变化时,它首先会被置为 null if (this.personRef) { // personRef 是 Person 类的实例 this.personRef.sayHello(); } }; setDivRef = (ref) => { this.divRef = ref; if (this.divRef) { // div ref 是 HTMLElement this.divRef.style.backgroundColor = 'lightgreen'; } }; render() { return ( <React.Fragment> <Person ref={this.setPersonRef} /> <div ref={this.setDivRef}>hi there</div> </React.Fragment> ); } }

Person实例上没有stylegetBoundingClientRect等 DOM 能力,而<div>的回调则能直接操作样式。理解这一点,是正确使用innerRef的前提。

一个常见的错误:把组件实例传给innerRef

请看下面这段“看起来没问题”的代码:

<Draggable draggableId="draggable-1" index={0}> {(provided, snapshot) => ( <Person ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} /> )} </Draggable>

它会让你的应用直接崩溃 💥!

原因正如上文所述:react-beautiful-dnd期望provided.innerRef收到的是组件的DOM 节点,而不是类组件的实例。这里我们传入的是Person的实例,底层在调用节点方法、读取节点几何信息时会立即失败。

仓库在开发模式下提供了明确的校验来拦截这类错误。看 src/view/check-is-valid-inner-ref.js,它断言innerRef收到的必须是一个HTMLElement

invariant( el && isHtmlElement(el), ` provided.innerRef has not been provided with a HTMLElement. ... `, );

isHtmlElement的实现(见 src/view/is-type-of-element/is-html-element.js)是对目标元素所在 window 的HTMLElementinstanceof判断:

export default function isHtmlElement(el: Object): boolean %checks { return el instanceof getWindowFromEl(el).HTMLElement; }

DraggableDroppable会在挂载及更新时通过各自的useValidation(见 src/view/draggable/use-validation.js 与 src/view/droppable/use-validation.js)调用该校验。对应的测试用例也验证了这一点:test/unit/view/droppable/inner-ref-validation.spec.js 确认“未提供 ref”时开发者会收到警告;inner-ref-validation.spec.js 确认把SVGElement传给innerRef时会抛出错误。

正确姿势:为自定义组件暴露一个 DOM ref

如果你把拖拽内容封装成了自定义组件,一个简单可靠的做法是给组件自定义一个innerRefprop,由它在渲染时把这个 prop 挂到组件内部真实的 DOM 元素上:

class Person extends React.Component { render() { return ( <div {...this.props} ref={this.props.innerRef}> I am a person, I think.. </div> ); } }

注意:innerRef只是一个约定俗成的名字。你也可以命名为domRef之类的其他名字,只要语义清晰即可。

然后,在<Draggable />中把provided.innerRef通过自定义 prop 传入,而不是通过ref

<Draggable draggableId="draggable-1" index={0}> {(provided, snapshot) => ( <Person - ref={provided.innerRef} + innerRef={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} > <h4>My draggable</h4> </Person> )} </Draggable>

关于 styled-components v4 与 emotion v10+

如果你使用styled-components v4,其innerRefprop 已被ref取代——v4 基于 React 16 的forwardRefAPI,通过ref回调直接把 DOM 节点透传出来,而不是组件实例。emotion v10+同样弃用了innerRef,推荐使用带forwardRef透传的ref

消除 React 警告:避免把全部 props 铺到 DOM 节点上

上面“透传innerRefprop”的方案虽然简单,但会触发一个React 警告:我们通过{...this.props}把组件所有 props 都展开到了 DOM 节点上,其中包含innerRef这个自定义 prop——React 不允许你把这种非 DOM 属性添加到元素上。

正确的做法是把需要下发的 props 显式解构出来,只把真正的 DOM 相关 props 铺到元素上:

class Person extends React.Component { render() { - return ( - <div {...this.props} ref={this.props.innerRef}> - I am a person, I think.. - </div> - ); } } class Person extends React.Component { render() { + const { provided, innerRef } = this.props; + return ( + <div + {...provided.draggableProps} + {...provided.dragHandleProps} + ref={innerRef} + > + I am a person, I think.. + </div> + ); } } <Draggable draggableId="draggable-1" index={0}> {(provided, snapshot) => ( <Person innerRef={provided.innerRef} - {...provided.draggableProps} - {...provided.dragHandleProps} + provided={provided} /> )} </Draggable>

注意这里把provided整体作为 prop 传入,由Person自行决定把draggablePropsdragHandleProps铺在哪个节点上。这样既避免了 React 警告,又保持了自定义组件对 DOM 结构的完全控制。

进阶:组件内部也需要 DOM 节点时怎么办

如果自定义组件自身也需要使用那个 DOM 节点(比如读取自身尺寸、注册事件),可以编写一个更强大的 ref 设置函数,同时完成两件事:把节点保存为自己的实例属性,并把它转发给react-beautiful-dnd

class Person extends React.Component { setRef = (ref) => { // 把 dom ref 保存为实例属性 this.ref = ref; // 把 dom ref 交给 react-beautiful-dnd this.props.innerRef(ref); }; render() { const { provided, innerRef } = this.props; return ( <div {...provided.draggableProps} {...provided.dragHandleProps} ref={this.setRef} > I am a person, I think.. </div> ); } }

这样this.ref与库内部的innerRef指向同一个节点,组件内外的测量结果保持一致。这也是对库内部setRef模式(先写入ref.current、再由getRef读取)的一种用户侧复刻。

关于 SVG 的重要提示

react-beautiful-dnd不支持直接拖拽<svg>元素:SVGElement并不实现HTMLElement,且在各浏览器上的焦点管理行为不一致、甚至缺失(如 IE11 上调用svgElement.focus()会抛异常),这与库“漂亮且无障碍的拖拽”核心价值相悖。

如果你需要拖拽 SVG 图形,请把<svg>包进一个HTMLElement(如<span><div>),以获得良好的无障碍支持与跨浏览器兼容性:

// ✅ 支持:用 span 包裹 svg,并把 innerRef 挂在 span 上 <Draggable draggableId="supported" index={0}> {(provided) => ( <span {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef} > <svg {/* SVG 内容 */} /> </span> )} </Draggable>

更多替代方案(<img>标签、background-image等)请参阅仓库中的 dragging SVGs 指南。

小结:一条铁律

回顾整篇指南,核心只有一条规则:

provided.innerRef必须收到一个真实的HTMLElement,绝不能是组件实例、SVGElement 或其他任何对象。

  • 直接渲染 JSX 元素时,把ref={provided.innerRef}挂在元素上即可;
  • 使用自定义组件时,通过自定义 prop(如innerRef)把 DOM 节点透传出来,避免{...this.props}引发的 React 警告;
  • 若使用 styled-components v4 或 emotion v10+,优先使用基于forwardRefref透传;
  • 拖拽 SVG 时,务必用HTMLElement包裹,并参照 dragging SVGs 指南 的推荐方案。

仓库在开发模式下内置的checkIsValidInnerRef校验(src/view/check-is-valid-inner-ref.js)与相关测试(test/unit/view/droppable/inner-ref-validation.spec.js)会第一时间帮你发现传错 ref 的问题。只要遵循上述模式,你就能把任意自定义组件安全地接入 react-beautiful-dnd 的拖拽体系。

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

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

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

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

立即咨询