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 陷阱。
为什么Draggable和Droppable需要innerRef
react-beautiful-dnd 是一个基于 DOM 测量的拖拽库:在拖拽开始前,它需要通过getBoundingClientRect()读取被拖拽项与可放置容器的位置、尺寸,并据此计算位移与排序。因此,<Draggable />与<Droppable />都必须拿到一个真实的HTMLElement,而这个节点正是通过DraggableProvided和DroppableProvided对象上的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实例上没有style、getBoundingClientRect等 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 的HTMLElement做instanceof判断:
export default function isHtmlElement(el: Object): boolean %checks { return el instanceof getWindowFromEl(el).HTMLElement; }Draggable与Droppable会在挂载及更新时通过各自的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自行决定把draggableProps、dragHandleProps铺在哪个节点上。这样既避免了 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+,优先使用基于
forwardRef的ref透传; - 拖拽 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),仅供参考