[Next.js]react-dndとdnd-kitでは、next/linkをdraggableにしたときの挙動が異なる
概要
Next.js製アプリケーションにドラッグアンドドロップ機能を実装しようとしたのですが、draggableにする対象がnext/linkだった際にreact-dndとdnd-kitに差異があったのでシェアする記事です。
対象バージョン
- Next.js 13.4.12
- react-dnd 16.0.1
- dnd-kit 6.0.8
観測できた事象
next/linkまたはnext/linkを含む要素をDraggableにするために双方のライブラリで実装した際、以下のように挙動に違いが生まれ、結果としてreact-dndのほうが望ましいという結果が得られました。
- dnd-kitの場合、要素をクリックした際ページ全体がリロードされる(History APIに基づいたルーティングの挙動をしない)
- dnd-kitの場合、ドラッグアンドドロップした直後にも全体がリロードされる
観測結果を証明するリポジトリ
動作内容の動画
以上の動画のように、画面上部のreact-dndによる実装は諸々意図通りに動いていますが、下部のdnd-kitの場合はなにか操作するたびにページ全体にリロードが走っています。
実装概要
ドラッグする要素
import Link from "next/link";
export const Item = ({name}: Props) => {
return (
<Link href={'/after'} className={'p-4 cursor-pointer flex justify-center items-center'}>
<span>
{name}
</span>
</Link>
)
};
ドラッグさせるためにuseDraggableを親に噛ましているコンポーネント(dnd-kitの場合)
※なおnext/link側にlistenersやattributesを設定しても改善しませんでした
const DragArea = ({item}: { item: TItem }) => {
const {setNodeRef, isDragging, listeners, transform, attributes } = useDraggable({
id: item.name,
})
const style: CSSProperties = {
transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined,
}
return (
<div ref={setNodeRef} {...listeners} style={style} {...attributes} className={isDragging ? 'opacity-50' : ''}>
<Item name={item.name}></Item>
</div>
)
}
同コンポーネント(react-dndの場合)
const DragArea = ({item}: { item: TItem }) => {
const [collect, ref] = useDrag({
type: 'item',
collect: (monitor) => ({
isDragging: monitor.isDragging()
}),
item: item
})
return (
<div ref={ref} className={collect.isDragging ? 'opacity-50' : ''}>
<Item name={item.name}></Item>
</div>
)
}
結論
上記より、next/linkをドラッグアンドドロップする必要がある場合はreact-dndのほうが安定して動作しそうだといえます。
dnd-kitでもできるのか、以下の内容を試したのですがいずれも動作が改善しませんでした。
- listeners/attributesをバインドする要素を変える
- dnd-kitのuseDraggableフックの引数の内容を調べ、本挙動を回避できないか調べる
考察
ドラッグアンドドロップを実現するライブラリで動作に差があるというのも奇妙です。
後日腰を据えて調べてみようとは思うのですが、現時点で自分が思っている仮説を挙げておきます。
まず前提として、next/linkは(おそらく)通常のaタグとは異なり、クリックやKey Down時のイベントをハンドリングしてHistory APIを実行しているはずです。
その状況において、dnd-kitはフックから返ってきたlistenersをバインドしているわけですから、このオブジェクト内部にイベントハンドラが格納されており、next/linkへイベントが伝播される前に伝播を止めてドラッグアンドドロップの挙動を始めているのではないでしょうか。
listenersをconsole.logしたところ、onKeyDownとonPointerDownが設定されていました。また、listenersを外すことで、next/linkの正しい挙動が再現できたため、listenersの設定が本挙動に絡んでいそうです。
加えて、listenersを外して() => void 0
を設定しても、next/linkは動いたので、やはりイベントの伝播を意図的に止めるような実装がされているのではないでしょうか。
雰囲気多分このへんかなと思いました。PointerSensorの基底クラスですし、stopPropagationとかしているので怪しいです。ただちょっと10分くらい読んだ程度では全体が読みきれませんでした。この手のライブラリを組む人すごすぎます・・・
オンライン家庭教師マナリンクを運営するスタートアップNoSchoolのテックブログです。 manalink.jp/ 創業以来年次200%前後で売上成長しつつ、技術面・組織面での課題に日々向き合っています。 カジュアル面談はこちら! forms.gle/fGAk3vDqKv4Dg2MN7
Discussion