はじめに
セクション分けされたリストにおけるDndを@dnd-kitで実装するとき、グローバルな SortableContext を定義しているのに、なぜ各セクション内にも必要なのか? という疑問が生まれると思います。
直感的には全アイテムIDを含むグローバル SortableContext があれば十分のように思えますが、グローバルのみではアニメーションにガタつきが発生してしまいます。
本記事では、その原因と「グローバル」と「ローカル」の二重構造による解決策を解説します。
問題の構造
グローバルな SortableContext を定義し、セクションごとに DroppableContainer を配置するだけでは、ドラッグ時にアニメーションがガタつく問題が発生します。
// NG: セクション内にSortableContextがない
<SortableContext items={allItemIds}>
{sections.map(([section, items]) => (
<div key={section}>
<DroppableContainer id={section}>
{items.map(item => <SortableItem item={item} />)}
</DroppableContainer>
</div>
))}
</SortableContext>
原因
SortableContext には2つの役割があります
- 異なるコンテキスト間でのドラッグ認識(グローバル)
- 同一コンテキスト内でのアニメーション計算(ローカル)
グローバルだけではドラッグ可能なアイテムの認識はできても、セクション内での位置関係を正しく計算できません。
解決策
各セクション内にもローカルな SortableContext を追加します。
// OK: セクション内にもSortableContextを追加
<SortableContext items={allItemIds}>
{sections.map(([section, items]) => (
<div key={section}>
<SortableContext items={items.map(item => `sortable-item-${item.id}`)}>
<DroppableContainer id={section}>
{items.map(item => <SortableItem item={item} />)}
</DroppableContainer>
</SortableContext>
</div>
))}
</SortableContext>
なぜこれで動くのか
- グローバル:セクション間のドラッグを可能にする
- ローカル:セクション内でのアニメーション計算を正しく行う
@dnd-kit/sortable は最も近い祖先の SortableContext から情報を取得するため、この二重構造でクロスセクションのドラッグとスムーズなアニメーションを両立できます。