HMR breaks active drag operations: stale source references and duplicate items on provider remount
- 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
Assessment
This issue has not been assessed yet.