Skip to content

useDragAndDrop

What this solves

Drag and drop is common in admin UIs:

  • Reordering items in a list (tasks, menu items, priorities)
  • Moving items between lists (kanban columns, category assignment)
  • Sortable tables or grids

Ad-hoc implementations often have:

  • Inconsistent drag feedback and drop zones
  • Complex state management for active/over items
  • Duplicated reorder logic across features

useDragAndDropList and useDragAndDropMultiList provide consistent drag state management built on @dnd-kit.


When to use it

Use these hooks when you need:

  • useDragAndDropList — Single list reordering (sortable list, priority queue)
  • useDragAndDropMultiList — Multi-list with cross-list movement (kanban boards, category assignment)

When NOT to use it

  • For simple drag-to-upload or file drop zones (use native drag events or a file upload library)
  • When you need complex nested drag targets (consider using @dnd-kit directly with custom sensors)

Basic usage

Single list reordering

tsx
import { useDragAndDropList } from '@/shared/hooks'
import { DndContext, closestCenter } from '@dnd-kit/core'
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'

type Task = { id: string; title: string }

export function TaskList({ tasks }: { tasks: Task[] }) {
  const {
    items,
    itemIds,
    activeItem,
    handleDragStart,
    handleDragEnd,
    handleDragCancel,
  } = useDragAndDropList({
    items: tasks,
    getItemId: (task) => task.id,
    onReorder: (reorderedItems) => {
      // Persist new order
      console.log('New order:', reorderedItems)
    },
  })

  return (
    <DndContext
      collisionDetection={closestCenter}
      onDragStart={({ active }) => handleDragStart(active.id)}
      onDragEnd={({ active, over }) => handleDragEnd(active.id, over?.id ?? null)}
      onDragCancel={handleDragCancel}
    >
      <SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
        {items.map((task) => (
          <SortableItem key={task.id} id={task.id}>
            {task.title}
          </SortableItem>
        ))}
      </SortableContext>
    </DndContext>
  )
}

Multi-list (kanban-style)

tsx
import { useDragAndDropMultiList } from '@/shared/hooks'

type Card = { id: string; title: string }
type Column = { id: string; title: string; cards: Card[] }

export function KanbanBoard({ columns }: { columns: Column[] }) {
  const {
    activeItem,
    handleDragStart,
    handleDragEnd,
    handleDragCancel,
  } = useDragAndDropMultiList({
    lists: columns,
    getListId: (col) => col.id,
    getListItems: (col) => col.cards,
    getItemId: (card) => card.id,
    onMoveWithinList: (listId, items, activeId, overId) => {
      // Handle reorder within same column
    },
    onMoveBetweenLists: (fromListId, toListId, itemId, overItemId) => {
      // Handle move between columns
    },
  })

  // Render columns with DndContext...
}

Companion components

Katalyst provides presentational components in src/shared/ui/components/DragAndDrop.tsx:

  • DraggableItem — Wrapper for draggable items with drag state styling
  • DragHandle — Accessible drag handle with grab cursor
  • DroppableZone — Drop target with visual feedback
  • SortableList — Complete sortable list with built-in DnD context

These components integrate with the hooks and provide consistent visual feedback.


API reference (concise)

useDragAndDropList

ts
interface UseDragAndDropListOptions<T> {
  items: T[]
  getItemId: (item: T) => UniqueIdentifier
  onReorder?: (items: T[], activeId: UniqueIdentifier, overId: UniqueIdentifier) => void
}

interface UseDragAndDropListResult<T> {
  items: T[]
  itemIds: UniqueIdentifier[]
  activeItem: T | null
  handleDragStart: (id: UniqueIdentifier) => void
  handleDragEnd: (activeId: UniqueIdentifier, overId: UniqueIdentifier | null) => void
  handleDragCancel: () => void
  reorderItems: (activeId: UniqueIdentifier, overId: UniqueIdentifier) => T[]
}

useDragAndDropMultiList

ts
interface UseDragAndDropMultiListOptions<T, L> {
  lists: L[]
  getListId: (list: L) => UniqueIdentifier
  getListItems: (list: L) => T[]
  getItemId: (item: T) => UniqueIdentifier
  onMoveWithinList?: (listId, items, activeId, overId) => void
  onMoveBetweenLists?: (fromListId, toListId, itemId, overItemId) => void
}

  • useFilter — Often combined with drag and drop for filtered, sortable lists