重构 DraggableEntry 、修正卡片高度对齐 by cyfung1031 · Pull Request #1245 · scriptscat/scriptcat
重新再看了一次
把名字改成 DraggableInjector
避免你误以为是 forwardRef
这次重构用的是 CloneElement
我们来重新梳理一下 React.cloneElement 和 forwardRef 在 dnd-kit 拖拽场景里的区别,以及为什么你的代码选择 cloneElement 是正确的。
一、React.cloneElement 的核心作用
React.cloneElement 的作用非常明确:
它拿一个已存在的 React 元素,复制一份,然后用你提供的新 props 去覆盖原来的 props,最后返回这个新的元素。
原来的元素完全不受影响。
关键点在于:
- 它可以覆盖任何 props,包括 ref
- 这是普通方式(比如直接 {...props} 展开)做不到的,因为 ref 不是普通属性
代码示例:
const original = <div className="old-class">内容</div>; const cloned = React.cloneElement(original, { className: "new-class", ref: someRef, });
结果就是新的 div,className 被覆盖,ref 也被设置,原对象不变。
二、在 dnd-kit 场景中为什么必须用它
你的组件本质上是一个“行为增强器”:
- 给 children 注入拖拽能力(attributes、style、ref)
- 不能增加额外的 DOM 层(否则布局、flex/grid、CSS 选择器都会出问题)
- dnd-kit 要求 ref 必须直接挂在可拖拽的真实 DOM 元素上
所以你写了:
const mergedRef = composeRefs(setNodeRef, children.ref); return React.cloneElement(children, { ...attributes, ref: mergedRef, style: { ...children.props.style, ...style }, });
这三步做到了:
- 保留 children 原来的 ref(如果有的话)
- 合并 dnd-kit 的 setNodeRef
- 注入拖拽所需的 attributes 和 style
- 没有多一层 DOM,布局和结构保持原样
三、如果用
{children}
会怎样很多人第一反应是包一层 div:
<div ref={setNodeRef} {...attributes} style={style}> {children} </div>
问题立刻出现:
- 多了一层 DOM,flex/grid 布局可能错位(子元素变成孙元素)
- CSS 选择器(比如 :first-child、nth-child)失效
- dnd-kit 有时会因为 ref 没挂在最外层真实元素而出问题
- 性能和可维护性都变差
cloneElement 完美绕过了这些问题。
四、forwardRef 为什么不适合这里
forwardRef 的设计目的是让父组件能把 ref 传给子组件内部的某个 DOM。
典型用法:
const MyInput = forwardRef((props, ref) => ( <input ref={ref} {...props} /> ));
它是“从外向内传递 ref”的工具。
但你的需求正好相反:
你需要从 children 身上读取它已有的 ref(如果使用者传了 ref),然后和 dnd-kit 的 ref 合并,再写回去。
forwardRef 完全读不到 children 的 ref。它只能处理从父组件传进来的 ref。
如果你把组件改成 forwardRef 包一层:
- 你拿到的 ref 是父组件传给你的
- children 原有的 ref 被忽略
- dnd 的 setNodeRef 还是挂不到正确的元素上
方向完全反了。
五、ref 合并的必要性
如果使用者这样用你的组件:
<Draggable> <Card ref={myCardRef} /> </Draggable>
children 已经有 ref 了。
你再直接 ref={setNodeRef} 会覆盖掉 myCardRef,导致外部拿不到 Card 的实例。
所以必须用 composeRefs(或 useMergedRef)把两个 ref 合并:
const mergedRef = composeRefs(setNodeRef, children.ref);
cloneElement 能把这个 mergedRef 正确覆盖回去,这是 forwardRef 做不到的。
六、总结对比
| 需求 | forwardRef 是否能解决 | cloneElement + composeRefs 是否能解决 |
|---|---|---|
| 父组件传 ref 给内部 DOM | 是 | 否 |
| 读取 children 已有的 ref | 否 | 是 |
| 合并两个 ref 并覆盖回去 | 否 | 是 |
| 不增加额外 DOM 层 | 通常会增加 | 是 |
| 适合 dnd-kit 这种场景 | 不适合 | 非常适合 |
七、结论
你的场景是典型的“无 DOM 行为注入”:
- 不产出额外节点
- 增强 children 的能力
- 保留使用者原有的 ref 和 props
React 生态里这种模式非常常见(Radix UI、Headless UI、很多拖拽库的 wrapper 都这么做)。
cloneElement + ref 合并 是目前最干净、最符合需求的解法。
forwardRef 在这里不但帮不上忙,反而会让 ref 指向错误的方向。