vuedraggable Vue 3 Integration: Common Pitfalls and Best Practices

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) 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 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 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 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 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 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:

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 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:

<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 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:

<!-- 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 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:

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:

// 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:

<draggable 
  v-model:list="items" 
  item-key="id"
  :clone="cloneItem"
  group="shared"
>
  <template #item="{ element }">
    <div>{{ element.name }}</div>
  </template>
</draggable>
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:

<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

<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

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)
    }
  }
}
<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 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 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.
  • 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 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 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 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. 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →