# vuedraggable Vue 3 Integration: Common Pitfalls and Best Practices

> Master vuedraggable Vue 3 integration. Avoid common pitfalls and implement best practices using reactive arrays, unique keys, and v-model:list for seamless drag and drop.

- Repository: [Vue/core](https://github.com/vuejs/core)
- Tags: best-practices
- Published: 2026-02-16

---

**Successful vuedraggable Vue 3 integration requires reactive array references, stable unique keys for every item, and the modern `v-model:list` syntax to ensure Vue's `patchKeyedChildren` algorithm correctly reorders DOM nodes without remounting components.**

Integrating drag-and-drop functionality into Vue 3 applications often leads to subtle reactivity bugs when developers misunderstand how the framework's renderer synchronizes DOM state with data. This guide examines how vuedraggable Vue 3 implementations interact with Vue's core reactivity system and keyed diffing algorithm to help you avoid common integration failures.

## How vuedraggable Interacts with Vue 3 Core

`vuedraggable` works by synchronizing a **reactive array** with the order of DOM elements created by Vue's renderer. When the component renders, Vue's **keyed-children diff algorithm** (`patchKeyedChildren` in [`packages/runtime-core/src/renderer.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts)) decides whether an element should be moved, inserted, or removed. If the array or keys are not configured correctly, Vue cannot compute minimal DOM updates, resulting in items disappearing, `v-model` bindings failing to emit, or unnecessary re-mounts that reset component state.

### Critical Vue Core Files for vuedraggable Integration

| File | Role in vuedraggable Vue 3 Integration |
|------|----------------------------------------|
| [`packages/runtime-dom/src/directives/vModel.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-dom/src/directives/vModel.ts) | Implements the `v-model` directive that stores the assigner function on the vnode and invokes it on input events. `vuedraggable` relies on this to push array updates back to parent components. |
| [`packages/runtime-core/src/renderer.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts) | Contains the `patchKeyedChildren` algorithm that efficiently reorders DOM nodes based on stable keys—essential for drag-and-drop reordering without state loss. |
| [`packages/reactivity/src/ref.ts`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/ref.ts) | Defines `ref()` which creates reactive containers for lists bound to `vuedraggable`. Without this reactivity, `v-model` updates fail silently. |
| [`packages/runtime-core/src/compat/componentVModel.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/compat/componentVModel.ts) | Compatibility layer for legacy `v-model` usage; demonstrates why the modern `v-model:list` syntax prevents prop naming confusion. |
| [`packages/runtime-core/src/helpers/useModel.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/helpers/useModel.ts) | Helper for `defineModel`/`useModel` that normalizes prop names to `modelValue`, useful when building custom wrappers around `vuedraggable`. |

## Common vuedraggable Vue 3 Pitfalls

Understanding how Vue core processes your data helps diagnose these frequent integration failures:

| Symptom | Root Cause | Vue Core Mechanism |
|---------|------------|-------------------|
| **List does not update after drag** | Bound array is not a reactive `ref`/`reactive` object | No reactive trigger means `vModelDynamic` never calls the assigner, so `update:modelValue` is not emitted |
| **Items lose internal state (inputs reset)** | Using loop index as `:key` | `patchKeyedChildren` treats them as different nodes, unmounting old components and mounting fresh ones |
| **Duplicate items appear** | Mutating the same array reference inside a watcher or `@change` without creating a new array | Vue sees the same reference, performs no diff, and appends instead of replacing |
| **`v-model` fires on every mouse move** | Binding directly to a non-array prop (e.g., `v-model="dragged"`) | The `vModel` directive listens to `input` events, but the payload type mismatch causes erratic behavior in `vuedraggable` |
| **Parent does not receive `@update:modelValue`** | Parent component does not declare `emits: ['update:modelValue']` or uses old `modelValue` prop | Vue's runtime-core validates emitted events; undeclared calls are ignored in development mode |

## vuedraggable Vue 3 Best Practices

Follow these guidelines to align with Vue 3's reactivity and rendering model:

### 1. Declare Reactive Array References

Always wrap your list in `ref()` or `reactive()` to ensure the reactivity system tracks mutations:

```typescript
import { ref } from 'vue'

const items = ref([
  { id: 1, name: 'First' },
  { id: 2, name: 'Second' },
])

```

This guarantees that `vModelDynamic` in [`packages/runtime-dom/src/directives/vModel.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-dom/src/directives/vModel.ts) can invoke the assigner function when `vuedraggable` emits updates.

### 2. Use Stable, Unique Keys

Assign a stable `:key` using your item's primary identifier, not the loop index:

```html
<draggable v-model:list="items" item-key="id">
  <template #item="{ element }">
    <div :key="element.id">{{ element.name }}</div>
  </template>
</draggable>

```

This allows `patchKeyedChildren` in [`packages/runtime-core/src/renderer.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts) to perform DOM moves instead of destructive remounts.

### 3. Adopt Modern v-model:list Syntax

Use the Vue 3 specific `v-model:list` binding rather than legacy `v-model`:

```html
<!-- Correct for Vue 3 -->
<draggable v-model:list="items" />

<!-- Avoid: legacy syntax that may trigger compat layer issues -->
<draggable v-model="items" />

```

The compat layer in [`packages/runtime-core/src/compat/componentVModel.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/compat/componentVModel.ts) can introduce subtle prop naming confusion that breaks drag-and-drop updates.

### 4. Declare Emits in Parent Components

When using the Options API, explicitly declare the update event:

```javascript
export default {
  emits: ['update:modelValue'],
  data() {
    return {
      items: [{ id: 1, name: 'Alpha' }]
    }
  }
}

```

Vue's runtime-core validates emitted events; undeclared events are ignored in development mode, causing silent failures.

### 5. Avoid Direct Index Mutations

Never mutate arrays using index assignments that bypass reactivity tracking:

```typescript
// Wrong - Vue cannot detect this change
items.value[0] = { id: 99, name: 'New' }

// Correct - creates new array reference
items.value = [
  { id: 99, name: 'New' },
  ...items.value.slice(1)
]

```

Alternatively, use reactive array methods like `push`, `splice`, or `sort` that Vue's reactivity system wraps.

### 6. Handle Complex Objects with Clone Events

When working with nested data structures that require custom copy semantics, use the `clone` or `move` events rather than default mutations:

```html
<draggable 
  v-model:list="items" 
  item-key="id"
  :clone="cloneItem"
  group="shared"
>
  <template #item="{ element }">
    <div>{{ element.name }}</div>
  </template>
</draggable>

```

```typescript
function cloneItem(item: Item): Item {
  return { ...item, id: Date.now() } // Ensure unique ID for clone
}

```

### 7. Test Nested Draggable Components Carefully

When nesting draggable instances, ensure each level maintains unique keys scoped to that level:

```html
<draggable v-model:list="groups" item-key="groupId">
  <template #item="{ element: group }">
    <div :key="group.groupId">
      <h3>{{ group.name }}</h3>
      <draggable v-model:list="group.items" item-key="itemId">
        <template #item="{ element: item }">
          <div :key="item.itemId">{{ item.name }}</div>
        </template>
      </draggable>
    </div>
  </template>
</draggable>

```

Conflicting keys across nesting levels cause Vue's `patchKeyedChildren` to misidentify nodes, leading to erratic drag behavior.

## Implementation Examples

### Composition API with TypeScript

```vue
<script setup lang="ts">
import { ref } from 'vue'
import draggable from 'vuedraggable'

interface Task {
  id: number
  title: string
  status: 'todo' | 'done'
}

const tasks = ref<Task[]>([
  { id: 1, title: 'Review PR', status: 'todo' },
  { id: 2, title: 'Update docs', status: 'todo' },
  { id: 3, title: 'Deploy staging', status: 'done' },
])

const logUpdate = () => {
  console.log('New order:', tasks.value.map(t => t.id))
}
</script>

<template>
  <draggable
    v-model:list="tasks"
    item-key="id"
    class="task-list"
    @update="logUpdate"
  >
    <template #item="{ element }">
      <div :key="element.id" class="task-item">
        <span :class="element.status">{{ element.title }}</span>
      </div>
    </template>
  </draggable>
</template>

<style scoped>
.task-list { display: flex; flex-direction: column; gap: 0.5rem; }
.task-item { padding: 1rem; background: #f5f5f5; border-radius: 4px; cursor: move; }
.todo { color: #d32f2f; }
.done { color: #388e3c; text-decoration: line-through; }
</style>

```

### Options API with Explicit Event Declaration

```javascript
export default {
  components: { draggable },
  emits: ['update:modelValue'], // Critical for Vue 3 event validation
  data() {
    return {
      items: [
        { id: 1, name: 'Item A' },
        { id: 2, name: 'Item B' },
        { id: 3, name: 'Item C' },
      ]
    }
  },
  methods: {
    onDragEnd() {
      console.log('Drag ended, new order:', this.items)
    }
  }
}

```

```html
<template>
  <draggable 
    v-model:list="items" 
    item-key="id"
    @end="onDragEnd"
  >
    <template #item="{ element }">
      <div :key="element.id" class="draggable-item">
        {{ element.name }}
      </div>
    </template>
  </draggable>
</template>

```

## Summary

- **Use reactive references**: Wrap lists in `ref()` or `reactive()` so Vue's reactivity system in [`packages/reactivity/src/ref.ts`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/ref.ts) can track changes and trigger `v-model` updates.
- **Provide stable keys**: Always use unique item identifiers (like `id`) for `:key` and `item-key` props to enable `patchKeyedChildren` in [`packages/runtime-core/src/renderer.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts) to perform efficient DOM moves rather than destructive remounts.
- **Adopt modern syntax**: Use `v-model:list` instead of legacy `v-model` to avoid compatibility layer issues in [`packages/runtime-core/src/compat/componentVModel.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/compat/componentVModel.ts).
- **Declare emits explicitly**: When using the Options API, declare `emits: ['update:modelValue']` so Vue's runtime-core validates and forwards events correctly.
- **Avoid index mutations**: Never use index assignments on reactive arrays; instead use array methods or create new array references to ensure `vModelDynamic` in [`packages/runtime-dom/src/directives/vModel.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-dom/src/directives/vModel.ts) detects changes.

## Frequently Asked Questions

### Why does my list not update after dragging items in vuedraggable Vue 3?

This occurs when the bound array lacks reactivity. Vue's `vModelDynamic` directive in [`packages/runtime-dom/src/directives/vModel.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-dom/src/directives/vModel.ts) requires a reactive container to invoke the assigner function. If you bind a plain JavaScript array instead of a `ref()` or `reactive()` object, the `update:modelValue` event emits but the parent state never updates because no reactivity trigger exists.

### Why do my input fields reset when I drag items in vuedraggable?

This happens when you use the loop index as the `:key` prop instead of a stable unique identifier. Vue's `patchKeyedChildren` algorithm in [`packages/runtime-core/src/renderer.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts) uses keys to determine if a DOM node can be moved or must be recreated. When keys change during reordering (as indices do), Vue unmounts the old component and mounts a fresh one, destroying any internal state like form input values.

### What is the difference between v-model and v-model:list in vuedraggable Vue 3?

The `v-model:list` syntax is the native Vue 3 approach for array bindings, while legacy `v-model` relies on the compatibility layer in [`packages/runtime-core/src/compat/componentVModel.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/compat/componentVModel.ts). Using `v-model:list` explicitly tells Vue to treat the binding as a list update rather than a single value, preventing prop naming confusion that can cause the parent component to miss `update:modelValue` events during drag operations.

### How do I prevent vuedraggable from mutating my original array reference?

Always assign a new array reference when modifying the list outside of `vuedraggable`'s internal handlers. Vue's reactivity system in [`packages/reactivity/src/ref.ts`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/ref.ts) tracks references, not deep mutations. If you need to manually update the list, use `items.value = [...items.value, newItem]` or array methods like `push()` and `splice()` that Vue wraps with reactivity triggers, rather than direct index assignments like `items.value[0] = newItem` which bypass tracking.