diff --git a/apps/docs/docs/docs.json b/apps/docs/docs/docs.json index e426b1600..043067d83 100644 --- a/apps/docs/docs/docs.json +++ b/apps/docs/docs/docs.json @@ -97,7 +97,10 @@ "pages": [ "react/guides/migration", "react/guides/multiple-sortable-lists", - "react/guides/sortable-state-management" + "react/guides/sortable-state-management", + "react/guides/collision-detection", + "react/guides/modifiers", + "react/guides/feedback" ] } ] diff --git a/apps/docs/docs/extend/sensors/keyboard-sensor.mdx b/apps/docs/docs/extend/sensors/keyboard-sensor.mdx index 2eaed965f..99a3a98e8 100644 --- a/apps/docs/docs/extend/sensors/keyboard-sensor.mdx +++ b/apps/docs/docs/extend/sensors/keyboard-sensor.mdx @@ -13,7 +13,7 @@ The Keyboard sensor enables keyboard-based drag and drop interactions. It is ena ```ts import {DragDropManager} from '@dnd-kit/dom'; -import {KeyboardSensor} from '@dnd-kit/dom/sensors'; +import {KeyboardSensor} from '@dnd-kit/dom'; const manager = new DragDropManager({ sensors: [ diff --git a/apps/docs/docs/extend/sensors/pointer-sensor.mdx b/apps/docs/docs/extend/sensors/pointer-sensor.mdx index 3bfceae33..69f40aee7 100644 --- a/apps/docs/docs/extend/sensors/pointer-sensor.mdx +++ b/apps/docs/docs/extend/sensors/pointer-sensor.mdx @@ -201,6 +201,29 @@ The Pointer sensor handles these events: The sensor automatically binds listeners across all same-origin documents, enabling drag operations across same-origin iframes. +## Touch and mobile + +The Pointer sensor handles touch input on mobile devices by default — no additional setup is required. + +On touch devices, dragging activates after a **250ms delay** with **5px movement tolerance**. This prevents accidental drags when scrolling. You can customize these constraints for your use case: + +```ts +PointerSensor.configure({ + activationConstraints(event) { + if (event.pointerType === 'touch') { + return [ + new PointerActivationConstraints.Delay({ + value: 150, // Shorter delay for faster touch response + tolerance: 10, // More tolerance for finger movement + }), + ]; + } + + return undefined; // Use defaults for mouse/pen + }, +}); +``` + ### Best Practices 1. Use appropriate constraints for different input types: diff --git a/apps/docs/docs/react/components/drag-drop-provider.mdx b/apps/docs/docs/react/components/drag-drop-provider.mdx index 53be6dcdf..c21123a64 100644 --- a/apps/docs/docs/react/components/drag-drop-provider.mdx +++ b/apps/docs/docs/react/components/drag-drop-provider.mdx @@ -178,6 +178,40 @@ function App() { Called when collisions are detected. Call `event.preventDefault()` to prevent automatic target selection. +#### Event object structure + +All event handlers receive an event object and the `manager` instance. The most commonly used event, `onDragEnd`, has this shape: + +```ts +onDragEnd={(event, manager) => { + event.operation.source // The dragged element (Draggable), or null + event.operation.target // The drop target (Droppable), or null + event.operation.source.id // Unique identifier of the dragged element + event.operation.target?.id // Unique identifier of the drop target + event.operation.position // { current: {x, y}, initial: {x, y} } + event.operation.status // Status of the drag operation + event.canceled // true if the drag was cancelled (e.g. Escape key) + event.nativeEvent // The underlying browser event, if available +}} +``` + +For sortable elements, use the [`isSortable`](/react/guides/sortable-state-management#type-guards) type guard to access sortable-specific properties: + +```tsx +import {isSortable} from '@dnd-kit/react/sortable'; + +onDragEnd={(event) => { + const {source} = event.operation; + + if (isSortable(source)) { + source.index // Current position + source.initialIndex // Position when the drag started + source.group // Current group (for multi-list) + source.initialGroup // Group when the drag started + } +}} +``` + ### Configuration diff --git a/apps/docs/docs/react/guides/collision-detection.mdx b/apps/docs/docs/react/guides/collision-detection.mdx new file mode 100644 index 000000000..ea4e7215c --- /dev/null +++ b/apps/docs/docs/react/guides/collision-detection.mdx @@ -0,0 +1,148 @@ +--- +title: 'Collision detection' +metaTitle: 'Collision Detection - React | dnd kit' +description: 'Customize how draggable elements detect collisions with droppable targets in React. Includes closestCenter, closestCorners, pointerIntersection, and custom detectors.' +icon: 'arrows-to-circle' +--- + +## Overview + +When you drag something over a droppable target, dnd kit runs a **collision detection algorithm** to decide which target is currently being hovered. The default works well for most lists, but you can customize it per droppable when you need different behavior — for example, card stacks, grids, or nested containers. + +Built-in detectors are exported from the `@dnd-kit/collision` package. You don't need to install it explicitly — it's already a transitive dependency of `@dnd-kit/react`. + +## Built-in detectors + + + All built-in detectors are exported from `@dnd-kit/collision`. If you don't pass a `collisionDetector`, dnd kit uses [`defaultCollisionDetection`](#default) automatically — you only need this guide when you want to override it. + + +### `defaultCollisionDetection` + +The default. Runs `pointerIntersection` first, and falls back to `shapeIntersection` when the pointer isn't inside any droppable. + +### `pointerIntersection` + +High precision — a collision is only detected when the pointer is **inside** the droppable's bounding rectangle. Good for precise drop zones where you want the user to clearly be "over" a target. + +### `shapeIntersection` + +Returns the droppable with the **greatest overlap area** with the dragged element's shape. Good for large containers where any visual overlap should count as a collision. Ties are broken by distance to the pointer. + + + Shape intersection: the dragged element overlaps droppable rectangles and the one with the greatest overlap area is selected + + +### `closestCenter` + +Picks the droppable whose **center point** is closest to the dragged element's center. Ideal for card stacking or grids where items snap to the nearest slot. + + + Closest center: a line from the dragged element's center to the closest droppable's center + + +### `closestCorners` + +Picks the droppable with the smallest **average corner-to-corner distance**. More forgiving than `closestCenter` at the edges of a list — useful for vertical sortable lists where you want items to pick up a neighbor as soon as the corners approach. + + + Closest corners: lines drawn between the four corners of the dragged element and the corresponding corners of each droppable + + +### `pointerDistance` + +Returns the droppable whose center is closest to the **pointer coordinates** (not the dragged element). Useful when you want drop detection to follow the cursor rather than the dragged element. + +### `directionBiased` + +Only detects collisions in the **direction the user is dragging** (up, down, left, right). Useful when you want items to only be swapped when the user drags *toward* them — prevents jitter when hovering near the edge between two targets. + +## Configuring a detector + +The `collisionDetector` option is set **per droppable**. Pass any built-in detector (or a custom one) to [`useDroppable`](/react/hooks/use-droppable) or [`useSortable`](/react/hooks/use-sortable): + +```tsx +import {useDroppable} from '@dnd-kit/react'; +import {closestCenter} from '@dnd-kit/collision'; + +function DropTarget({id}) { + const {ref, isDropTarget} = useDroppable({ + id, + collisionDetector: closestCenter, + }); + + return
{isDropTarget ? 'Drop here' : 'Empty'}
; +} +``` + +The same option works on `useSortable`: + +```tsx +import {useSortable} from '@dnd-kit/react/sortable'; +import {closestCorners} from '@dnd-kit/collision'; + +function SortableItem({id, index}) { + const sortable = useSortable({ + id, + index, + collisionDetector: closestCorners, + }); + + return
Item {id}
; +} +``` + + + Because `collisionDetector` is configured per droppable rather than globally, different targets on the same page can use different algorithms. For example, a free-form canvas area might use `pointerIntersection` while a sortable list next to it uses `closestCenter`. + + +## Collision priority + +When multiple droppables overlap — for example, a sortable card inside a droppable column — you can bias which one "wins" using `collisionPriority`. Higher values take precedence. + +```tsx +import {useDroppable} from '@dnd-kit/react'; +import {CollisionPriority} from '@dnd-kit/abstract'; + +function Column({id, children}) { + const {ref} = useDroppable({ + id, + collisionPriority: CollisionPriority.Low, // Cards inside will win by default + }); + + return
{children}
; +} +``` + +The `CollisionPriority` enum is exported from `@dnd-kit/abstract` and provides named levels: `Lowest`, `Low`, `Normal`, `High`, `Highest`. You can also pass a plain number. + +## Writing a custom detector + +A collision detector is a function that receives the drag operation and a droppable, and returns a collision (or `null` if there's no collision). The return `value` is a score — higher scores win when multiple droppables collide. + +```tsx +import type {CollisionDetector} from '@dnd-kit/abstract'; +import {CollisionPriority, CollisionType} from '@dnd-kit/abstract'; + +const myDetector: CollisionDetector = ({dragOperation, droppable}) => { + if (!droppable.shape) return null; + + // Your logic here — return null for no collision, or an object: + return { + id: droppable.id, + value: 1, // Higher values win + type: CollisionType.Collision, + priority: CollisionPriority.Normal, + }; +}; +``` + +Pass it to any droppable: + +```tsx +useDroppable({id, collisionDetector: myDetector}); +``` + +For reference implementations, read the source of the built-in detectors in [`@dnd-kit/collision`](https://github.com/clauderic/dnd-kit/tree/main/packages/collision/src/algorithms). + +If you're coming from `@dnd-kit/core`, see the [Migration guide](/react/guides/migration#collision-detection) for legacy API mappings. diff --git a/apps/docs/docs/react/guides/feedback.mdx b/apps/docs/docs/react/guides/feedback.mdx new file mode 100644 index 000000000..5f34b9b9e --- /dev/null +++ b/apps/docs/docs/react/guides/feedback.mdx @@ -0,0 +1,129 @@ +--- +title: 'Feedback' +metaTitle: 'Feedback Plugin - React | dnd kit' +description: 'Configure drag feedback, clone overlays, drop animations, and troubleshoot common issues like duplicate items in React.' +icon: 'clone' +--- + +## Overview + +The [Feedback](/extend/plugins/feedback) plugin manages visual feedback during drag operations. It is included by default and handles element promotion to the browser's top layer and drop animations. + +This guide covers common React patterns and troubleshooting. + +## Configuring feedback globally + +To configure feedback for all draggable elements, pass a `plugins` function to [`DragDropProvider`](/react/components/drag-drop-provider): + +```tsx +import {DragDropProvider} from '@dnd-kit/react'; +import {Feedback} from '@dnd-kit/dom'; + +function App() { + return ( + [ + ...defaults, + Feedback.configure({dropAnimation: null}), + ]} + > + {/* All draggable elements will have no drop animation */} + + ); +} +``` + + + Use the function form `(defaults) => [...]` to extend the default plugins rather than replacing them. Passing a plain array replaces all default plugins, which may disable expected behavior like auto-scrolling and accessibility. + + +## Per-draggable feedback + +Configure feedback on individual elements via the `plugins` option in [`useDraggable`](/react/hooks/use-draggable) or [`useSortable`](/react/hooks/use-sortable): + +```tsx +import {useDraggable} from '@dnd-kit/react'; +import {Feedback} from '@dnd-kit/dom'; + +function CloneDraggable({id}: {id: string}) { + const {ref} = useDraggable({ + id, + plugins: [ + Feedback.configure({ + feedback: 'clone', + }), + ], + }); + + return
Drag me (clone stays behind)
; +} +``` + +## Feedback modes + +The `feedback` option controls how the element behaves during drag: + +| Mode | Behavior | +|------|----------| +| `'default'` | The element is promoted to the top layer and moves with the pointer | +| `'clone'` | A clone of the element stays in its original position while the original moves | +| `'move'` | The element moves without top layer promotion or a placeholder | +| `'none'` | No visual feedback from the Feedback plugin. Use this when rendering a custom [`DragOverlay`](/react/components/drag-overlay) | + +## Drop animations + +### Disabling the drop animation + +```tsx +Feedback.configure({dropAnimation: null}) +``` + +### Customizing the drop animation + +```tsx +Feedback.configure({ + dropAnimation: { + duration: 300, // milliseconds (default: 250) + easing: 'ease', // CSS easing (default: 'ease') + }, +}) +``` + +### Custom animation function + +For full control, provide a function that returns a `Promise`: + +```tsx +Feedback.configure({ + dropAnimation: async ({element, translate}) => { + await element.animate( + [{transform: `translate3d(${translate.x}px, ${translate.y}px, 0)`}, {transform: 'translate3d(0, 0, 0)'}], + {duration: 200, easing: 'ease-out'} + ).finished; + }, +}) +``` + +## Troubleshooting + +### Duplicate items after data refetch + +When using sortable lists with React Query or other data fetching libraries, you may see duplicate items after a refetch. This happens because the [OptimisticSortingPlugin](/concepts/sortable#optimistic-sorting) has moved DOM elements during the drag, and the refetched data renders new elements in the updated positions. + +See [Integration with external state](/react/guides/sortable-state-management#integration-with-external-state) for how to solve this by deferring state sync until the drag is complete. + +### Double animation or snap on drop + +If items snap into position twice after dropping, your state update is likely conflicting with the drop animation. The Feedback plugin animates the element back, then your state update triggers a re-render that moves it again. + +To fix this, disable the drop animation: + +```tsx +Feedback.configure({dropAnimation: null}) +``` + +Or ensure your state update matches the final position by using the [`move`](/react/guides/sortable-state-management) helper from `@dnd-kit/helpers` in `onDragEnd`, which correctly reconciles the optimistic position with your state. + + + For the complete API reference including `rootElement` and `keyboardTransition` options, see the [core Feedback plugin documentation](/extend/plugins/feedback). + diff --git a/apps/docs/docs/react/guides/migration.mdx b/apps/docs/docs/react/guides/migration.mdx index 99fc83d51..7bcb6e0ba 100644 --- a/apps/docs/docs/react/guides/migration.mdx +++ b/apps/docs/docs/react/guides/migration.mdx @@ -287,6 +287,101 @@ mode: 'wide' +## API reference + +A quick lookup for common legacy APIs and their new equivalents, organized by category. + +### Context & Provider + +| Legacy (`@dnd-kit/core`) | New (`@dnd-kit/react`) | +| --- | --- | +| `DndContext` | [`DragDropProvider`](/react/components/drag-drop-provider) | + +### Events + +| Legacy | New | +| --- | --- | +| `active` | `event.operation.source` | +| `over` | `event.operation.target` | +| `active.id` | `event.operation.source.id` | +| `onDragCancel` | Check `event.canceled` inside `onDragEnd` | + +### Sensors + + + The legacy `MouseSensor` and `TouchSensor` have been merged into a single [`PointerSensor`](/extend/sensors/pointer-sensor) that handles mouse, touch, and pen input via the native Pointer Events API. You can still apply different behavior per input type by passing a function to `activationConstraints` and branching on `event.pointerType` (`'mouse'`, `'touch'`, or `'pen'`): + + ```tsx + import {DragDropProvider} from '@dnd-kit/react'; + import {PointerSensor, PointerActivationConstraints} from '@dnd-kit/dom'; + + [ + ...defaults.filter((sensor) => sensor !== PointerSensor), + PointerSensor.configure({ + activationConstraints(event, source) { + if (event.pointerType === 'touch') { + return [ + new PointerActivationConstraints.Delay({value: 500, tolerance: {x: 5, y: 5}}), + ]; + } + return [new PointerActivationConstraints.Distance({value: 8})]; + }, + }), + ]} + > + {/* ... */} + + ``` + + If you don't configure it, the default already differentiates pointer types — see the [PointerSensor docs](/extend/sensors/pointer-sensor) for the full default behavior. + + +| Legacy (`@dnd-kit/core`) | New (`@dnd-kit/dom`) | +| --- | --- | +| `useSensor` / `useSensors` | Built-in by default. Customize via the `sensors` prop on [`DragDropProvider`](/react/components/drag-drop-provider) | +| `MouseSensor` | [`PointerSensor`](/extend/sensors/pointer-sensor) from `@dnd-kit/dom` | +| `TouchSensor` | [`PointerSensor`](/extend/sensors/pointer-sensor) from `@dnd-kit/dom` | +| `PointerSensor` | [`PointerSensor`](/extend/sensors/pointer-sensor) from `@dnd-kit/dom` | +| `KeyboardSensor` | [`KeyboardSensor`](/extend/sensors/keyboard-sensor) from `@dnd-kit/dom` | + +### Collision detection + +| Legacy | New | +| --- | --- | +| `collisionDetection` prop on `DndContext` | `collisionDetector` option on [`useDroppable`](/react/hooks/use-droppable) / [`useSortable`](/react/hooks/use-sortable) | +| `pointerWithin` | `pointerIntersection` from `@dnd-kit/collision` | +| `closestCenter` | `closestCenter` from `@dnd-kit/collision` | +| `closestCorners` | `closestCorners` from `@dnd-kit/collision` | + +### Modifiers + +See the [React modifiers guide](/react/guides/modifiers) for usage examples. + +| Legacy | New | +| --- | --- | +| `restrictToParentElement` | `RestrictToElement` from `@dnd-kit/dom/modifiers` | +| `restrictToWindowEdges` | `RestrictToWindow` from `@dnd-kit/dom/modifiers` | +| `restrictToVerticalAxis` | `RestrictToVerticalAxis` from `@dnd-kit/abstract/modifiers` | +| `restrictToHorizontalAxis` | `RestrictToHorizontalAxis` from `@dnd-kit/abstract/modifiers` | +| `createSnapModifier` | `SnapModifier` from `@dnd-kit/abstract/modifiers` | + +### Sortable + + + `SortableContext` is no longer needed. Sortable items register and coordinate with the [`DragDropProvider`](/react/components/drag-drop-provider) automatically when you use [`useSortable`](/react/hooks/use-sortable) — there's no wrapping context to configure. Use the `type` and `accept` options on `useSortable` to control which items can be sorted together. + + +See the [sortable state management guide](/react/guides/sortable-state-management) for usage examples. + +| Legacy | New | +| --- | --- | +| `SortableContext` | No longer needed — see note above | +| `arrayMove` | `move` from `@dnd-kit/helpers` | +| `verticalListSortingStrategy` | Not needed — handled automatically | +| `horizontalListSortingStrategy` | Not needed — handled automatically | +| `rectSortingStrategy` | Not needed — handled automatically | + ## Next steps @@ -298,24 +393,24 @@ mode: 'wide' Build beautiful drag and drop interfaces in minutes with the new dnd kit - Learn how to make elements draggable with the new API + Use the `move` helper from `@dnd-kit/helpers` or manage state manually - Create drop zones and handle collision detection + Restrict movement to an element, axis, or snap to a grid in React - Build sortable interfaces with array manipulation helpers + Configure drag feedback, clone overlays, and drop animations in React diff --git a/apps/docs/docs/react/guides/modifiers.mdx b/apps/docs/docs/react/guides/modifiers.mdx new file mode 100644 index 000000000..357b875f3 --- /dev/null +++ b/apps/docs/docs/react/guides/modifiers.mdx @@ -0,0 +1,188 @@ +--- +title: 'Modifiers' +metaTitle: 'Modifiers - React | dnd kit' +description: 'Restrict drag movement to an element, axis, or snap to a grid in React.' +icon: 'sliders' +--- + +## Overview + +[Modifiers](/extend/modifiers) transform the movement of draggable elements during drag operations. They can restrict movement to axes or boundaries, adjust positioning, or implement custom movement logic. + +This guide covers how to use modifiers in React with hooks like [`useDraggable`](/react/hooks/use-draggable) and [`useSortable`](/react/hooks/use-sortable). + +## Restricting to a container element + +Use the `RestrictToElement` modifier from `@dnd-kit/dom/modifiers` to constrain dragging within a container. Pass the container's DOM element via a React ref: + +```tsx +import {useRef} from 'react'; +import {DragDropProvider} from '@dnd-kit/react'; +import {useDraggable} from '@dnd-kit/react'; +import {RestrictToElement} from '@dnd-kit/dom/modifiers'; + +function App() { + const containerRef = useRef(null); + + return ( + +
+ +
+
+ ); +} + +function DraggableItem({container}: {container: React.RefObject}) { + const {ref} = useDraggable({ + id: 'draggable-1', + modifiers: [ + RestrictToElement.configure({ + element: container.current, + }), + ], + }); + + return
Drag me
; +} +``` + + + The `element` option also accepts a function that receives the current drag operation and returns an element. This is useful when you need dynamic container references: + + ```tsx + RestrictToElement.configure({ + element: () => document.getElementById('my-container'), + }) + ``` + + +## Restricting to the window + +Use `RestrictToWindow` from `@dnd-kit/dom/modifiers` to prevent dragging outside the browser viewport: + +```tsx +import {useDraggable} from '@dnd-kit/react'; +import {RestrictToWindow} from '@dnd-kit/dom/modifiers'; + +function DraggableItem() { + const {ref} = useDraggable({ + id: 'draggable-1', + modifiers: [RestrictToWindow], + }); + + return
Drag me
; +} +``` + +## Restricting to an axis + +Use `RestrictToVerticalAxis` or `RestrictToHorizontalAxis` from `@dnd-kit/abstract/modifiers` to lock movement to a single axis: + +```tsx +import {useSortable} from '@dnd-kit/react/sortable'; +import {RestrictToVerticalAxis} from '@dnd-kit/abstract/modifiers'; + +function SortableItem({id, index}: {id: string; index: number}) { + const {ref} = useSortable({ + id, + index, + modifiers: [RestrictToVerticalAxis], + }); + + return
Item {id}
; +} +``` + +## Snapping to a grid + +Use the `SnapModifier` from `@dnd-kit/abstract/modifiers` to snap movement to a grid: + +```tsx +import {useDraggable} from '@dnd-kit/react'; +import {SnapModifier} from '@dnd-kit/abstract/modifiers'; + +function DraggableItem() { + const {ref} = useDraggable({ + id: 'draggable-1', + modifiers: [ + SnapModifier.configure({ + size: 20, // Snap every 20px in both directions + }), + ], + }); + + return
Drag me
; +} +``` + +The `size` option accepts a number for uniform snapping, or an object with separate `x` and `y` values: + +```tsx +SnapModifier.configure({ + size: {x: 50, y: 25}, // 50px horizontal, 25px vertical +}) +``` + +## Combining modifiers + +You can combine multiple modifiers. They are applied in order: + +```tsx +import {useDraggable} from '@dnd-kit/react'; +import {RestrictToElement} from '@dnd-kit/dom/modifiers'; +import {SnapModifier} from '@dnd-kit/abstract/modifiers'; + +function DraggableItem({container}: {container: React.RefObject}) { + const {ref} = useDraggable({ + id: 'draggable-1', + modifiers: [ + RestrictToElement.configure({element: container.current}), + SnapModifier.configure({size: 20}), + ], + }); + + return
Drag me
; +} +``` + +## Global vs. per-draggable modifiers + +Modifiers can be set globally on the [`DragDropProvider`](/react/components/drag-drop-provider) to apply to all draggable elements, or on individual hooks for per-element control. + +### Global modifiers + +```tsx +import {DragDropProvider} from '@dnd-kit/react'; +import {RestrictToWindow} from '@dnd-kit/dom/modifiers'; + +function App() { + return ( + + {/* All draggable elements are restricted to the window */} + + ); +} +``` + +### Per-draggable modifiers + +Per-draggable modifiers take precedence over global modifiers: + +```tsx +import {useDraggable} from '@dnd-kit/react'; +import {RestrictToVerticalAxis} from '@dnd-kit/abstract/modifiers'; + +function VerticalOnlyDraggable() { + const {ref} = useDraggable({ + id: 'vertical-only', + modifiers: [RestrictToVerticalAxis], + }); + + return
I can only move vertically
; +} +``` + + + For the full list of built-in modifiers and how to create custom modifiers, see the [core Modifiers documentation](/extend/modifiers). + diff --git a/apps/docs/docs/react/guides/multiple-sortable-lists.mdx b/apps/docs/docs/react/guides/multiple-sortable-lists.mdx index 3b5865a5e..ed1d11039 100644 --- a/apps/docs/docs/react/guides/multiple-sortable-lists.mdx +++ b/apps/docs/docs/react/guides/multiple-sortable-lists.mdx @@ -1,7 +1,7 @@ --- title: 'Multiple sortable lists' metaTitle: 'Multiple Sortable Lists - React | dnd kit' -description: 'Learn how to reorder sortable elements across multiple lists.' +description: 'Learn how to build kanban boards, trello-like interfaces, and multi-column layouts with sortable drag and drop across multiple lists.' icon: 'columns-3' mode: "wide" --- @@ -13,7 +13,7 @@ import {CodeSandbox} from '/snippets/sandbox.mdx'; ## Overview -In this guide, you'll learn how to reorder sortable elements across multiple lists. This is useful when you have multiple lists and you want to move elements between them. +In this guide, you'll learn how to reorder sortable elements across multiple lists. This pattern is commonly used to build **kanban boards**, **trello-like task managers**, and **multi-column layouts** where items can be reordered within and across columns. Before getting started, make sure you familiarize yourself with the [useSortable](/react/hooks/use-sortable) hook. diff --git a/apps/docs/docs/react/guides/sortable-state-management.mdx b/apps/docs/docs/react/guides/sortable-state-management.mdx index 345ea9d13..9241f92ed 100644 --- a/apps/docs/docs/react/guides/sortable-state-management.mdx +++ b/apps/docs/docs/react/guides/sortable-state-management.mdx @@ -1,8 +1,8 @@ --- title: 'Managing sortable state' metaTitle: 'Managing Sortable State - React | dnd kit' -description: 'Learn how to manage sortable state with and without the move helper.' -icon: 'sliders' +description: 'Learn how to manage sortable state using the move helper or manual state management, including integration with external state libraries like React Query, Zustand, or Redux.' +icon: 'list-ol' --- ## Overview @@ -217,3 +217,52 @@ if (isSortableOperation(operation)) { operation.target.index; // typed } ``` + +## 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: + +```tsx +import {useState, useEffect, useRef} from 'react'; +import {DragDropProvider} from '@dnd-kit/react'; + +function SortableList() { + const {data: fetchedItems} = useQuery({queryKey: ['items'], queryFn: fetchItems}); + const [items, setItems] = useState(fetchedItems ?? []); + const isDragging = useRef(false); + + useEffect(() => { + if (fetchedItems && !isDragging.current) { + setItems(fetchedItems); + } + }, [fetchedItems]); + + return ( + { isDragging.current = true; }} + onDragEnd={(event) => { + isDragging.current = false; + + if (event.canceled) { + // Reset to server state on cancel + setItems(fetchedItems ?? []); + return; + } + + // Update local state, then sync with server + setItems((items) => move(items, event)); + }} + > + {items.map((item, index) => ( + + ))} + + ); +} +``` + + + 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. + diff --git a/apps/docs/docs/react/quickstart.mdx b/apps/docs/docs/react/quickstart.mdx index 84002fae1..a7efa60e5 100644 --- a/apps/docs/docs/react/quickstart.mdx +++ b/apps/docs/docs/react/quickstart.mdx @@ -35,6 +35,15 @@ Before getting started, make sure you install `@dnd-kit/react` in your project: ``` + + `@dnd-kit/react` is the only required package. For sortable lists, you'll also want `@dnd-kit/helpers` + for the [`move`](/react/guides/sortable-state-management) utility. Packages like `@dnd-kit/dom` and `@dnd-kit/abstract` are installed + automatically as dependencies. + + If you're migrating from the legacy `@dnd-kit/core`, `@dnd-kit/sortable`, or `@dnd-kit/utilities` + packages, see the [Migration guide](/react/guides/migration). + + ## Making elements draggable Let's get started by creating draggable elements that can be dropped over droppable targets. To do so, we'll be using the `useDraggable` hook.