Skip to main content

Overview

When building sortable interfaces, you need to keep your application state in sync with drag operations. There are two approaches:
  1. Using the move helper from @dnd-kit/helpers — a convenience function that takes your items and a drag event and returns a new array with the item moved to its new position. It supports flat arrays and grouped records, handles canceled drags, and works with optimistic sorting out of the box. This is covered in the Multiple sortable lists guide.
  2. Manual state management — using the sortable properties and type guards for full control over state updates.
This guide covers the second approach. Before reading this guide, make sure you’re familiar with optimistic sorting, which is enabled by default and affects how source and target behave during drag operations.

Understanding optimistic sorting

The OptimisticSortingPlugin is enabled by default for all sortable items. It optimistically reorders DOM elements during a drag so the UI feels responsive without requiring React re-renders on every dragover event. A key consequence is that source and target in the drag operation will refer to the same element during a drag. This means you cannot compare source.id and target.id to determine what moved. Instead, use the sortable-specific properties on the source:
These properties are available on the source when it is a sortable element. Use the isSortable type guard to narrow the type.
You can call event.preventDefault() in onDragOver to prevent the OptimisticSortingPlugin from optimistically updating for that specific event. This is useful when you want to conditionally block certain moves (for example, preventing items from being dragged into a specific group).

Single list without the move helper

With optimistic sorting, you only need to handle onDragEnd. The OptimisticSortingPlugin takes care of visual feedback during the drag.

Multiple lists without the move helper

For multiple lists, use initialGroup and group to detect whether the item stayed in the same list or moved to a different one:
When updating state in onDragOver (rather than onDragEnd), save a snapshot in onDragStart so you can revert if the drag is canceled.

Comparison with the move helper

Use the move helper when your data structure matches its expectations (flat arrays or Record<string, array>). Use manual state management when you need more control, have custom data structures, or use computed IDs.

Type guards

isSortable

Checks whether a Draggable or Droppable is a sortable instance, narrowing the type to expose index, initialIndex, group, and initialGroup:

isSortableOperation

Narrows both source and target of a drag operation at once:

Integration with external state

When using sortable lists alongside data fetching libraries like React Query, TanStack Query, or SWR, you may encounter duplicate items after a refetch. This happens when optimistic sorting has moved DOM elements during the drag, and then a refetch replaces the data while the drag state is still active. To avoid this, only sync your local items state with fetched data when no drag is in progress:
The key principle is maintaining a single source of truth: render from your local items state (not directly from the query data), and only update it from the query when no drag is active.