clauderic / clauderic/dnd-kit

HMR breaks active drag operations: stale source references and duplicate items on provider remount

Open
#1,963 0 comments 1 reaction 0 assignees View on GitHub
bug dom svelte triage:done
Dominant language
TypeScript
Stars
17.6k
Forks
924
Avg merge
2d 10h
Merged PRs (30d)
2

Description

## HMR breaks active drag operations - elements lose tracking and duplicates appear

We've been running into a frustrating issue with HMR in the development of our app. Whenever Vite does a hot update while a drag is happening (or even when our app reorders DOM elements inside a `DragDropProvider`), everything falls apart, the dragged element loses its visual feedback, snaps to a wrong position, or we get duplicate items after dropping.

**Stack:** Svelte 5 + Vite + `@dnd-kit-svelte` with `@dnd-kit/dom@0.3.2`

---

### What happens

The simplest way to trigger this is to start dragging something, then save a file. But it also happens without saving files, in our case we have sortable tabs across multiple windows, and when a window gets brought to the front (DOM reorder via Svelte's `{#each}`), the components inside it get destroyed and recreated. This has the same effect as an HMR update from dnd-kit's perspective.

When the provider remounts:

1. `useDraggable`/`useSortable` create new instances with the same IDs, but the active `dragOperation.source` still points to the old (destroyed) instance. So `isDragging` on the new instance is permanently `false`, `Feedback` loses its element reference, and the drag visual breaks.
2. For cross-container sortable, both the old and new instances can end up processing `dragend`, which leads to the dropped item appearing in two places.

---

### How to reproduce

1. Set up a `DragDropProvider` with sortable items across multiple containers
2. Start dragging an item from container A
3. Either save a file (triggering HMR) or do anything that causes Svelte to destroy/recreate the component tree inside the provider
4. The dragged element loses position tracking, and dropping it can produce duplicates

---

### Root cause analysis

When the provider remounts, `useOnValueChange` fires for plugins, sensors, and modifiers, which reassigns them on the manager. This recreates all plugin instances, and the `Feedback` plugin's internal `@dnd-kit/state` effects get severed. On top of that, the new draggable instances created by `useDraggable` have different object references than what `dragOperation.source` is tracking, so all the state checks break.

---

### Current workaround

We create the `DragDropManager` outside the provider and pass it via the `manager` prop, then lock the `plugins`/`sensors`/`modifiers` setters so the provider can't overwrite them:

```typescript
for (const prop of ['plugins', 'sensors', 'modifiers'] as const) {
let proto = Object.getPrototypeOf(manager);
let desc;
while (proto && !desc) {
desc = Object.getOwnPropertyDescriptor(proto, prop);
proto = Object.getPrototypeOf(proto);
}
if (desc?.get) {
Object.defineProperty(manager, prop, {
get: desc.get,
set() {},
configurable: true
});
}
}
```

This stops the plugin recreation issue but doesn't fix the stale draggable reference problem. For that we've had to set `hmr: false` in our Vite config, which obviously isn't great for DX.

---

### Suggested fixes

- **Skip reassignment**: Don't reassign plugins/sensors/modifiers when the provider remounts with the same manager and the config hasn't actually changed
- **Idempotent registration**: If a draggable instance with the same ID already exists, reuse it instead of creating a new one
- **Stable mode**: A `stable` flag on the provider that tells it not to touch the manager's config when a custom manager is passed

---

Reproduction repo: https://github.com/freddyamarante/dnd-kit-hmr-repro

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.