---
# === IDENTITY ===
id: software/migrations/jquery-to-vue/2026
canonical_question: "How do I migrate a jQuery codebase to Vue 3?"
aliases:
  - "jQuery to Vue migration guide"
  - "convert jQuery to Vue 3"
  - "replace jQuery with Vue"
  - "modernize jQuery frontend to Vue"
  - "incremental jQuery to Vue 3 migration"
  - "jQuery to Vue component mapping"
entity_type: software_reference
domain: software > migrations > jquery_to_vue
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-05-29
confidence: 0.90
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "Vue 3.5 (Sep 2024) — reactive props destructure stabilized"
  next_review: 2026-11-25
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Never mix jQuery DOM manipulation and Vue reactivity on the same DOM node — Vue's virtual DOM will overwrite jQuery changes on re-render"
  - "Vue 3 requires the full build (vue.global.js or vue.esm-browser.js) when using in-DOM templates during incremental migration — the runtime-only build excludes the template compiler"
  - "Always destroy jQuery plugins in Vue's unmounted/onUnmounted lifecycle hook to prevent memory leaks and ghost DOM elements"
  - "Do not use v-html to inject jQuery-rendered HTML — it bypasses Vue reactivity and creates XSS vulnerabilities"
  - "jQuery 4.0 slim build removes Deferreds/Callbacks — if your codebase uses $.Deferred, use the full jQuery 4.0 build or migrate to native Promises before switching to Vue composables"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Migrating jQuery to React (JSX-based component model)"
    use_instead: "software/migrations/jquery-to-react/2026"
  - condition: "Replacing jQuery with vanilla JavaScript without a framework"
    use_instead: "software/migrations/jquery-to-vanilla-js/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: codebase_size
    question: "How large is the jQuery codebase? (lines of jQuery-dependent JS)"
    type: choice
    options: ["< 500 lines", "500-5000 lines", "5000-20000 lines", "> 20000 lines"]
  - key: migration_strategy
    question: "Do you prefer incremental migration (coexistence) or a full rewrite?"
    type: choice
    options: ["incremental", "full_rewrite"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/jquery-to-vue/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-29)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/migrations/jquery-to-react/2026"
      label: "jQuery to React Migration"
    - id: "software/migrations/jquery-to-vanilla-js/2026"
      label: "jQuery to Vanilla JavaScript"
  solves:
    - id: "software/migrations/javascript-to-typescript/2026"
      label: "JavaScript to TypeScript Migration"
  alternative_to:
    - id: "software/migrations/jquery-to-react/2026"
      label: "jQuery to React Migration"
  often_confused_with:
    - id: "software/migrations/jquery-to-vanilla-js/2026"
      label: "jQuery to Vanilla JavaScript"

# === SOURCES (9 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Creating a Vue Application"
    author: Vue.js
    url: https://vuejs.org/guide/essentials/application.html
    type: official_docs
    published: 2024-09-01
    reliability: authoritative
  - id: src2
    title: "Reactivity Fundamentals"
    author: Vue.js
    url: https://vuejs.org/guide/essentials/reactivity-fundamentals.html
    type: official_docs
    published: 2024-09-01
    reliability: authoritative
  - id: src3
    title: "How To Migrate From jQuery to Vue 3"
    author: Telerik
    url: https://www.telerik.com/blogs/how-to-migrate-jquery-vue-3
    type: technical_blog
    published: 2023-05-15
    reliability: high
  - id: src4
    title: "Incrementally Migrating a Legacy jQuery Web Application to a Vue.js SPA"
    author: Ricardo Ferreira
    url: https://medium.com/infraspeak/incrementally-migrating-a-legacy-jquery-web-application-to-a-vue-js-spa-ecbd6671beee
    type: technical_blog
    published: 2023-02-20
    reliability: high
  - id: src5
    title: "Custom Directives"
    author: Vue.js
    url: https://vuejs.org/guide/reusability/custom-directives.html
    type: official_docs
    published: 2024-09-01
    reliability: authoritative
  - id: src6
    title: "Announcing Vue 3.5"
    author: Evan You
    url: https://blog.vuejs.org/posts/vue-3-5
    type: official_docs
    published: 2024-09-01
    reliability: authoritative
  - id: src7
    title: "jQuery 4.0.0"
    author: jQuery Team
    url: https://blog.jquery.com/2026/01/17/jquery-4-0-0/
    type: official_docs
    published: 2026-01-17
    reliability: authoritative
  - id: src8
    title: "Migrating from jQuery+UI to VueJs"
    author: Thomas Amsler
    url: https://medium.com/valtech-ch/migrating-from-jquery-ui-to-vuejs-d35994e6ef31
    type: technical_blog
    published: 2023-04-10
    reliability: moderate_high
  - id: src9
    title: "Release v3.6.0-beta.1 (Vapor Mode + alien-signals reactivity)"
    author: vuejs/core
    url: https://github.com/vuejs/core/releases/tag/v3.6.0-beta.1
    type: official_docs
    published: 2025-12-23
    reliability: authoritative
---

# How to Migrate a jQuery Codebase to Vue 3

## How do I migrate a jQuery codebase to Vue 3?

## TL;DR

- **Bottom line**: Migrate incrementally -- mount Vue 3 apps into jQuery-managed pages using `createApp().mount()`, replace jQuery patterns one widget at a time with reactive state and directives, then remove jQuery when no code depends on it.
- **Key tool/command**: `createApp({ setup() { ... } }).mount('#vue-mount')`
- **Watch out for**: Direct DOM manipulation inside Vue components -- mixing `$(el).html()` with Vue's reactivity system causes the virtual DOM to desync from the real DOM.
- **Works with**: Vue 3.3+ (3.5 recommended), jQuery 1.x-4.x, works with Vite, Webpack, or plain script tags (CDN).

## Constraints

<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Never mix jQuery DOM manipulation and Vue reactivity on the same DOM node -- Vue's virtual DOM will overwrite jQuery changes on re-render.
- Vue 3 requires the full build (`vue.global.js` or `vue.esm-browser.js`) when using in-DOM templates during incremental migration -- the runtime-only build excludes the template compiler.
- Always destroy jQuery plugins in Vue's `unmounted`/`onUnmounted` lifecycle hook to prevent memory leaks and ghost DOM elements.
- Do not use `v-html` to inject jQuery-rendered HTML -- it bypasses Vue reactivity and creates XSS vulnerabilities.
- jQuery 4.0 slim build removes Deferreds/Callbacks -- if your codebase uses `$.Deferred`, use the full jQuery 4.0 build or migrate to native Promises before switching to Vue composables. [src7]

## Quick Reference

| jQuery Pattern | Vue 3 Equivalent | Example |
|---|---|---|
| `$('#el').html(val)` | `ref()` + template interpolation | `const text = ref('')` -> `<p>{{ text }}</p>` |
| `$('#el').on('click', fn)` | `v-on` / `@click` directive | `<button @click="handleClick">` |
| `$('#el').toggle()` | `v-if` / `v-show` directive | `<div v-show="isVisible">` |
| `$('#el').addClass('active')` | `:class` binding | `<div :class="{ active: isActive }">` |
| `$('#el').css({color: 'red'})` | `:style` binding | `<div :style="{ color: textColor }">` |
| `$('#el').val()` | `v-model` two-way binding | `<input v-model="searchQuery" />` |
| `$.ajax({url, success})` | `fetch` + `onMounted` / composable | `onMounted(async () => { data.value = await fetch(url).then(r => r.json()) })` |
| `$('#el').animate()` | `<Transition>` / `<TransitionGroup>` | `<Transition name="fade"><div v-if="show">...</div></Transition>` |
| `$(document).ready(fn)` | `onMounted(() => {})` | Runs after component is inserted into DOM |
| `$.each(arr, fn)` | `v-for` directive | `<li v-for="item in items" :key="item.id">{{ item.name }}</li>` |
| `$('#el').find('.child')` | Template refs + `useTemplateRef()` | `const el = useTemplateRef('container')` (Vue 3.5+) [src6] |
| `$('#form').serialize()` | `v-model` + reactive object | `const form = reactive({ name: '', email: '' })` |
| `$('#el').show()` / `.hide()` | `v-show` (CSS toggle) | `<div v-show="isVisible">` (keeps DOM, toggles `display`) |
| `$.Deferred()` | Native `Promise` / `async`/`await` | `const data = await fetch(url).then(r => r.json())` |
| `$('#plugin').pluginName()` | Custom directive or composable | `app.directive('tooltip', { mounted(el) { ... } })` |

## Decision Tree

```
START
├── Is the jQuery codebase < 500 lines with simple event handling?
│   ├── YES → Skip Vue, replace jQuery calls with vanilla JS (see jquery-to-vanilla-js)
│   └── NO ↓
├── Is this a full rewrite or incremental migration?
│   ├── FULL REWRITE → Scaffold new Vue 3 project (npm create vue@latest), rebuild from scratch
│   └── INCREMENTAL ↓
├── Does the page have isolated UI widgets (modals, tabs, forms)?
│   ├── YES → Start with "Vue Islands": mount Vue apps into existing page via createApp().mount()
│   └── NO ↓
├── Does the jQuery code heavily mutate shared global state (window.appState, data-* attrs)?
│   ├── YES → First extract state into a Pinia store or provide/inject, then migrate UI
│   └── NO ↓
├── Are there jQuery plugins with no Vue equivalent?
│   ├── YES → Wrap them in a custom directive using mounted/unmounted hooks (see Step 6)
│   └── NO ↓
├── Is the app server-rendered (PHP, Rails, Django templates)?
│   ├── YES → Use Vue's in-DOM template mode: createApp with existing HTML as template
│   └── NO ↓
└── DEFAULT → Migrate page-by-page: replace jQuery selectors with Vue components, use v-model for forms, replace $.ajax with fetch/composables
```

## Decision Logic

Structured if/then rules for agent-driven recommendations. Each rule maps a concrete migration condition to an action.

### If the jQuery codebase is under 500 lines with only simple event handlers
--> Skip Vue entirely; replace jQuery calls with vanilla JS (`querySelector`, `addEventListener`, `fetch`). [src3]

### If you need a no-build-step incremental migration on a server-rendered page (PHP, Rails, Django)
--> Load Vue 3 via CDN (`vue.global.prod.js`) and mount widgets with `createApp(...).mount('#id')`; the full build is required because in-DOM templates need the compiler. [src1, src4]

### If the page has isolated UI widgets (modals, tabs, forms) embedded in jQuery
--> Migrate as "Vue Islands": mount one `createApp()` per widget into its own container, leave the rest to jQuery, and remove jQuery only after every island is migrated. [src1, src4]

### If jQuery code stores shared state in the DOM (`data-*` attrs, hidden inputs, classes)
--> Extract that implicit state into a Pinia store or a shared `reactive()` bridge object first, then migrate the UI on top of it. [src4]

### If the app depends on a jQuery plugin with no Vue equivalent (date picker, rich-text editor, chart)
--> Wrap the plugin in a Vue custom directive, init in `mounted`, sync in `updated`, and always `destroy` in `unmounted` to prevent leaks. [src5, src8]

### If you are starting a green-field rewrite and want maximum runtime performance
--> Scaffold with `npm create vue@latest` on Vue 3.5 (stable), and evaluate Vue 3.6 Vapor Mode (`<script setup vapor>`) only for new, self-contained components — it is feature-complete but still unstable as of mid-2026. [src6, src9]

### If your codebase calls `$.Deferred` and you plan to use the jQuery 4.0 slim build
--> Convert Deferreds to native Promises before removing jQuery, since the 4.0 slim build drops Deferreds/Callbacks; otherwise stay on the full 4.0 build during migration. [src7]

## Step-by-Step Guide

### 1. Audit the jQuery surface area

Grep your codebase to quantify the migration scope. Count AJAX calls, event bindings, DOM manipulations, and plugin usages. This converts a vague rewrite into discrete, estimable tasks. [src3]

```bash
# Count jQuery usage patterns across the codebase
grep -rn '\$\.' --include='*.js' --include='*.html' | wc -l
grep -rn '\$\.ajax\|\.get(\|\.post(' --include='*.js' | wc -l
grep -rn '\.on(\|\.click(\|\.submit(' --include='*.js' | wc -l
grep -rn '\.html(\|\.text(\|\.val(\|\.append(' --include='*.js' | wc -l
```

**Verify**: Review the counts -- a typical medium app has 50-200 jQuery call sites. Prioritize pages by traffic and complexity.

### 2. Add Vue 3 alongside jQuery

Install Vue 3 into your existing project without removing jQuery. Both libraries can coexist on the same page because Vue only manages DOM inside its mount container. [src1]

```bash
# Option A: npm install (for bundler-based projects)
npm install vue@3

# Option B: CDN (for no-build-step projects -- ideal for incremental migration)
# Add to HTML:
# <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
```

**Verify**: `import { createApp } from 'vue'` compiles without errors, or `window.Vue` is available in the browser console.

### 3. Create mount points and mount Vue apps

Add container elements where Vue will take over. jQuery continues managing everything else on the page. Vue 3 supports multiple independent app instances on one page. [src1, src4]

```html
<!-- Existing jQuery page -->
<div id="legacy-header"><!-- jQuery manages this --></div>
<div id="vue-search-widget"></div>  <!-- Vue mounts here -->
<div id="legacy-content"><!-- jQuery manages this --></div>
```

```javascript
// mount-search.js -- mount Vue into existing page
import { createApp, ref } from 'vue';

const SearchWidget = {
  setup() {
    const query = ref('');
    const results = ref([]);

    async function search() {
      const res = await fetch(`/api/search?q=${encodeURIComponent(query.value)}`);
      results.value = await res.json();
    }

    return { query, results, search };
  },
  template: `
    <div>
      <input v-model="query" @keyup.enter="search" placeholder="Search..." />
      <button @click="search">Search</button>
      <ul>
        <li v-for="item in results" :key="item.id">{{ item.title }}</li>
      </ul>
    </div>
  `
};

const container = document.getElementById('vue-search-widget');
if (container) {
  createApp(SearchWidget).mount(container);
}
```

**Verify**: The Vue component renders within the existing page layout without breaking jQuery functionality. Inspect the mount element -- it should contain Vue-rendered DOM.

### 4. Replace jQuery DOM manipulation with Vue reactive state

Convert imperative DOM updates to declarative Vue state using `ref()` and `reactive()`. This is the core of the migration -- stop reading from the DOM and let Vue's reactivity system drive UI updates. Vue 3.5 improved reactivity memory usage by 56%. [src2, src3, src6]

```javascript
// BEFORE: jQuery
let count = 0;
$('#counter').text(count);
$('#increment').on('click', function() {
  count++;
  $('#counter').text(count);
});

// AFTER: Vue 3 Composition API
import { createApp, ref } from 'vue';

const Counter = {
  setup() {
    const count = ref(0);
    const increment = () => count.value++;
    return { count, increment };
  },
  template: `
    <div>
      <span>{{ count }}</span>
      <button @click="increment">Increment</button>
    </div>
  `
};

createApp(Counter).mount('#counter-mount');
```

**Verify**: Remove the jQuery code for that widget, confirm the Vue version renders and behaves identically. Check Vue DevTools -- state should be reactive.

### 5. Replace $.ajax with fetch or a Vue composable

Convert AJAX calls to the modern fetch API wrapped in a reusable composable. Composables are Vue's equivalent of React hooks -- reusable stateful logic extracted into functions. Use `onWatcherCleanup()` (Vue 3.5+) for automatic request cancellation inside watchers. [src3, src6]

```javascript
// composables/useFetch.js -- reusable data fetching composable
import { ref, onMounted, onUnmounted } from 'vue';

export function useFetch(url) {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(true);
  let controller = null;

  async function fetchData() {
    controller = new AbortController();
    loading.value = true;
    error.value = null;
    try {
      const res = await fetch(url, { signal: controller.signal });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      data.value = await res.json();
    } catch (err) {
      if (err.name !== 'AbortError') error.value = err.message;
    } finally {
      loading.value = false;
    }
  }

  onMounted(fetchData);
  onUnmounted(() => controller?.abort());

  return { data, error, loading, refetch: fetchData };
}

// Usage in a component
const UserList = {
  setup() {
    const { data: users, loading, error } = useFetch('/api/users');
    return { users, loading, error };
  },
  template: `
    <p v-if="loading">Loading...</p>
    <p v-else-if="error">Error: {{ error }}</p>
    <ul v-else>
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  `
};
```

**Verify**: Network tab shows the same API requests. Compare response handling with the old jQuery version. Unmounting the component should abort in-flight requests.

### 6. Wrap jQuery plugins using custom directives

For plugins without Vue alternatives (date pickers, rich text editors, legacy charting libraries), wrap them in a Vue custom directive using the `mounted` and `unmounted` lifecycle hooks. [src5, src8]

```javascript
// directives/v-chosen.js -- wraps jQuery Chosen plugin
import $ from 'jquery';
import 'chosen-js';

export const vChosen = {
  mounted(el, binding) {
    const $el = $(el);
    $el.chosen({ width: '100%', ...binding.value });

    // Sync changes back to Vue
    $el.on('change', () => {
      el.dispatchEvent(new Event('change'));
    });
  },
  updated(el) {
    $(el).trigger('chosen:updated');
  },
  unmounted(el) {
    $(el).chosen('destroy');
  }
};

// Register globally
import { createApp } from 'vue';
const app = createApp(App);
app.directive('chosen', vChosen);
app.mount('#app');

// Use in template
// <select v-model="selectedValue" v-chosen="{ search_contains: true }">
//   <option v-for="opt in options" :key="opt.value" :value="opt.value">
//     {{ opt.label }}
//   </option>
// </select>
```

**Verify**: Plugin initializes on mount, updates when Vue state changes, and destroys cleanly on unmount. Check browser console for errors and memory leaks via DevTools heap snapshot.

### 7. Remove jQuery and clean up

Once all components are migrated and no code references jQuery, uninstall it and remove all script tags. If migrating from jQuery 3.x, consider whether jQuery 4.0 compatibility changes affect your remaining code. [src3, src4, src7]

```bash
# Uninstall jQuery
npm uninstall jquery

# Remove from HTML
# <script src="jquery.min.js"></script>  <- delete this line

# Search for any remaining $ or jQuery references
grep -rn '\$(\|jQuery(' --include='*.js' --include='*.vue' --include='*.html'

# Check your bundler output for jQuery
npx vite build --mode production 2>&1 | grep -i jquery
```

**Verify**: `grep -rn 'jquery' --include='*.json' --include='*.js' --include='*.vue'` returns zero results. The application runs without jQuery loaded. Bundle size should decrease by ~19.5-30 KB (gzipped, depending on jQuery version).

## Code Examples

### JavaScript/Vue 3: Full page migration from jQuery to Vue SFC

> Full script: [javascript-vue-3-full-page-migration-from-jquery-t.js](scripts/javascript-vue-3-full-page-migration-from-jquery-t.js) (46 lines)

```javascript
// Input:  A jQuery page with a search form, results list, and loading spinner
// Output: Equivalent Vue 3 Single File Component with Composition API
// SearchPage.vue
<script setup>
import { ref, computed } from 'vue';
# ... (see full script)
```

### JavaScript/Vue 3: Bridge store for jQuery-Vue coexistence

> Full script: [javascript-vue-3-bridge-store-for-jquery-vue-coexi.js](scripts/javascript-vue-3-bridge-store-for-jquery-vue-coexi.js) (31 lines)

```javascript
// Input:  A legacy jQuery page where Vue needs to read/write shared state
// Output: A reactive bridge that lets jQuery code and Vue components share data
// bridge-store.js -- shared store for jQuery <-> Vue communication
import { reactive, watch } from 'vue';
// Create a reactive store accessible from both jQuery and Vue
# ... (see full script)
```

### TypeScript/Vue 3: Type-safe composable replacing jQuery AJAX patterns

> Full script: [typescript-vue-3-type-safe-composable-replacing-jq.ts](scripts/typescript-vue-3-type-safe-composable-replacing-jq.ts) (51 lines)

```typescript
// Input:  A set of jQuery $.ajax calls with consistent error handling
// Output: A type-safe Vue 3 composable with automatic loading/error state
// composables/useApi.ts
import { ref, type Ref } from 'vue';
interface UseApiReturn<T> {
# ... (see full script)
```

## Anti-Patterns

### Wrong: Direct DOM manipulation inside Vue components

```javascript
// ❌ BAD -- jQuery DOM manipulation inside a Vue component
const Counter = {
  mounted() {
    $('#count').text(0);
    $('#increment').on('click', () => {
      const current = parseInt($('#count').text());
      $('#count').text(current + 1);
    });
  },
  template: `
    <div>
      <span id="count">0</span>
      <button id="increment">+1</button>
    </div>
  `
};
```

### Correct: Use Vue reactive state for all UI updates

```javascript
// ✅ GOOD -- Vue reactivity drives the UI
const Counter = {
  setup() {
    const count = ref(0);
    return { count };
  },
  template: `
    <div>
      <span>{{ count }}</span>
      <button @click="count++">+1</button>
    </div>
  `
};
```

### Wrong: Using jQuery event delegation inside Vue

```javascript
// ❌ BAD -- jQuery event delegation alongside Vue
const ListComponent = {
  mounted() {
    $(this.$el).on('click', '.list-item', function() {
      $(this).toggleClass('selected');
    });
  },
  template: `<ul><li class="list-item" v-for="item in items" :key="item.id">{{ item.name }}</li></ul>`
};
```

### Correct: Handle events with Vue directives and reactive state

```javascript
// ✅ GOOD -- Vue handles events and state
const ListItem = {
  props: ['item'],
  setup() {
    const selected = ref(false);
    const toggle = () => { selected.value = !selected.value; };
    return { selected, toggle };
  },
  template: `
    <li :class="{ 'list-item': true, selected }" @click="toggle">
      {{ item.name }}
    </li>
  `
};
```

### Wrong: Using $.ajax inside Vue without lifecycle cleanup

```javascript
// ❌ BAD -- No cancellation, possible state update after unmount
const UserProfile = {
  props: ['userId'],
  setup(props) {
    const user = ref(null);
    watch(() => props.userId, (id) => {
      $.ajax({
        url: `/api/users/${id}`,
        success: (data) => { user.value = data; }
      });
    }, { immediate: true });
    return { user };
  },
  template: `<div>{{ user?.name }}</div>`
};
```

### Correct: Use fetch with AbortController in a composable

```javascript
// ✅ GOOD -- Proper cleanup prevents memory leaks and race conditions
import { ref, watch, onUnmounted } from 'vue';

const UserProfile = {
  props: ['userId'],
  setup(props) {
    const user = ref(null);
    let controller = null;

    watch(() => props.userId, async (id) => {
      controller?.abort();
      controller = new AbortController();
      try {
        const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
        user.value = await res.json();
      } catch (err) {
        if (err.name !== 'AbortError') console.error(err);
      }
    }, { immediate: true });

    onUnmounted(() => controller?.abort());
    return { user };
  },
  template: `<div>{{ user?.name }}</div>`
};
```

### Wrong: Replacing entire jQuery pages at once with Vue

```javascript
// ❌ BAD -- Trying to wrap an entire jQuery page in one Vue app
const app = createApp({
  mounted() {
    // Re-initialize all jQuery plugins inside Vue
    initDatePickers();
    initModals();
    initDataTables();
    initCharts();
  },
  template: `<div id="entire-page" v-html="legacyHtml"></div>`
});
```

### Correct: Mount multiple small Vue apps into the existing page

```javascript
// ✅ GOOD -- Replace one widget at a time with its own Vue app
// Each app is independent, self-contained, and testable

const searchApp = createApp(SearchWidget);
searchApp.mount('#search-mount');

const chartApp = createApp(ChartWidget);
chartApp.mount('#chart-mount');

// Rest of the page is still managed by jQuery
// Remove jQuery only after ALL widgets are migrated
```

### Wrong: Using this.$refs like jQuery selectors

```javascript
// ❌ BAD -- Treating refs as jQuery-style DOM query shortcuts
const FormComponent = {
  mounted() {
    // Manually manipulating DOM via refs defeats Vue's purpose
    this.$refs.nameInput.style.border = '2px solid red';
    this.$refs.nameInput.value = 'default';
    this.$refs.submitBtn.disabled = true;
  },
  template: `
    <form>
      <input ref="nameInput" />
      <button ref="submitBtn">Submit</button>
    </form>
  `
};
```

### Correct: Use reactive state and directive bindings

```javascript
// ✅ GOOD -- Let Vue's reactivity system manage everything
import { ref, computed } from 'vue';

const FormComponent = {
  setup() {
    const name = ref('default');
    const hasError = ref(true);
    const canSubmit = computed(() => name.value.length > 0 && !hasError.value);
    return { name, hasError, canSubmit };
  },
  template: `
    <form>
      <input v-model="name" :class="{ error: hasError }" />
      <button :disabled="!canSubmit">Submit</button>
    </form>
  `
};
```

## Common Pitfalls

- **Vue and jQuery both managing the same DOM node**: Vue's virtual DOM and jQuery's direct DOM manipulation conflict, causing the UI to desync. When Vue re-renders, it overwrites jQuery's changes; when jQuery modifies DOM, Vue's next patch may revert them. Fix: Give each library its own DOM subtree -- Vue renders into mount points that jQuery never touches. [src1]
- **Forgetting to destroy jQuery plugins on Vue unmount**: jQuery plugins attach event listeners and create DOM elements that persist after Vue unmounts the component, causing memory leaks and ghost UI. Fix: Always clean up in `unmounted` (Options API) or `onUnmounted` (Composition API): `$(el).plugin('destroy')`. [src5, src8]
- **Migrating everything at once instead of incrementally**: Big-bang rewrites stall feature development for months and introduce regression risk. Fix: Migrate one widget/page at a time. Ship each migration to production before starting the next. A typical 50-page app takes 3-6 months incrementally vs. 6-12 months for a rewrite. [src3, src4]
- **Not extracting implicit state from the DOM first**: In jQuery apps, the DOM is the state -- hidden inputs, `data-*` attributes, element classes, and text content all carry application state. Fix: Before migrating UI, extract this implicit state into JavaScript variables, a Pinia store, or a shared reactive object. [src4]
- **Using `v-html` to inject jQuery-rendered HTML**: This bypasses Vue's reactivity system and is an XSS risk. Fix: Convert jQuery-generated HTML to Vue templates with proper data binding. If you must render raw HTML temporarily, sanitize it with DOMPurify. [src2]
- **Mixing jQuery's `$(document).ready()` with Vue mounting**: Creates unnecessary coupling and can cause race conditions if jQuery tries to manipulate elements that Vue hasn't rendered yet. Fix: Use Vue's `onMounted` lifecycle hook for initialization. For bridge scenarios, mount Vue apps from a plain `<script>` after the DOM is ready. [src3]
- **Ignoring Vue DevTools during migration**: Without Vue DevTools, debugging reactivity issues is nearly impossible during the coexistence phase. Fix: Install Vue DevTools browser extension immediately. Use it to inspect component state, track event flow, and verify that reactivity is working. [src2]
- **Using jQuery 4.0 slim build with code that depends on $.Deferred**: jQuery 4.0 slim removes Deferreds and Callbacks. If your codebase uses `$.Deferred()`, either use the full jQuery 4.0 build during migration or convert Deferreds to native Promises first. Fix: `const promise = new Promise((resolve, reject) => { /* ... */ })`. [src7]

## Diagnostic Commands

```bash
# Count remaining jQuery references in the codebase
grep -rn '\$(\|jQuery\|\.ajax\|\.on(' --include='*.js' --include='*.vue' --include='*.html' | wc -l

# Find jQuery script tags in HTML files
grep -rn 'jquery\|jQuery' --include='*.html' | grep -i 'script'

# Check if jQuery 4.0 removed APIs are in use (migrate before removing jQuery)
grep -rn 'jQuery\.isArray\|jQuery\.parseJSON\|jQuery\.trim\|jQuery\.type\|jQuery\.now' --include='*.js' | wc -l

# Check bundle size for jQuery (if using Vite)
npx vite build 2>&1 | grep -i jquery
# Or inspect the bundle
npx rollup-plugin-visualizer  # Vite uses Rollup

# Verify Vue is rendering correctly (browser console)
# document.querySelectorAll('[data-v-app]').length

# List all mounted Vue app instances (browser console)
# document.querySelectorAll('.__vue_app__') // Vue 3 internal marker

# Check for memory leaks from jQuery plugins (Chrome DevTools)
# Performance tab -> Record -> Interact -> Check heap snapshots for detached DOM nodes

# Verify Vue 3.5 features are available
# In browser console: Vue.version (should be "3.5.x" or higher)
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Vue 3.6 (beta, Dec 2025) | Beta / unstable | Vapor Mode (no virtual DOM), `@vue/reactivity` refactored on alien-signals | 100% opt-in via `<script setup vapor>` + `createVaporApp`; interop with vDOM apps via `vaporInteropPlugin`. Stable release projected Q4 2026 — do not target for production migrations yet. [src9] |
| Vue 3.5 (Sep 2024) | Current (stable) | Reactive props destructure stable, `useTemplateRef()` added, `onWatcherCleanup()` added | Recommended version for new migrations. 56% less memory usage. [src6] |
| Vue 3.4 (Dec 2023) | Maintained | `defineModel` stable | Simplifies v-model on custom components |
| Vue 3.3 (May 2023) | Maintained | `defineOptions` macro, generic components | Minimum recommended version for new migrations |
| Vue 3.0-3.2 (2020-2022) | Maintenance | Composition API, `<script setup>` | `<script setup>` stabilized in 3.2 -- use it |
| Vue 2.7 (Jul 2022) | EOL (Dec 2023) | Backported Composition API | Do not target Vue 2 for new migrations |
| jQuery 4.0 (Jan 2026) | Current | Removed `$.isArray`, `$.parseJSON`, `$.trim`, `$.type`, `$.now`; slim build removes Deferreds; migrated to ES modules | Can coexist with Vue 3. Check upgrade guide before migrating. [src7] |
| jQuery 3.x (2016-2024) | Maintained | Removed deprecated APIs | Can coexist with Vue 3 during migration |
| jQuery 1.x-2.x | EOL | IE-specific code | Upgrade to 3.x/4.x first, or remove directly |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building component-driven SPAs with reactive data | Static sites with 2-3 event handlers | Vanilla JS or Alpine.js |
| Team values single-file components and template syntax | Team strongly prefers JSX | React (see jquery-to-react) |
| Codebase uses server-rendered HTML (PHP, Django, Rails) | Project is already React/Angular | Stay with current framework |
| Need incremental migration without build step (CDN) | jQuery codebase is <500 lines total | Replace jQuery calls with vanilla JS |
| App has complex forms requiring two-way binding | Project is a throwaway prototype | Keep jQuery |
| Team is small and needs gentle learning curve | Need SSR with streaming and React Server Components | React/Next.js |

## Important Caveats

- Vue 3 requires the full build (`vue.global.js` or `vue.esm-browser.js`) if using in-DOM templates (HTML as template source). The runtime-only build does not include the template compiler. This is critical for incremental migration where Vue uses existing HTML.
- jQuery 3.x `$.Deferred` is not fully Promise/A+ compliant -- convert to native Promises or `async`/`await` when migrating AJAX calls to Vue composables. jQuery 4.0 slim build removes Deferreds entirely. [src7]
- Vue's `<Transition>` component only works with elements toggled by `v-if` or `v-show` -- it does not replace jQuery's `.animate()` for arbitrary CSS property tweening. Use CSS transitions or a library like GSAP for complex animations.
- Bundle size during coexistence: Vue 3 adds ~33 KB gzipped alongside jQuery's ~19.5-30 KB (depending on version/build). Total overhead is temporary. Remove jQuery as soon as all references are migrated.
- Vue DevTools does not show components mounted via `createApp().mount()` unless the app is stored in a variable accessible from the page. During migration, keep app references in a global object for debugging: `window.__vueApps = { search: searchApp }`.
- jQuery 4.0 migrated source from AMD to ES modules and switched to Rollup for packaging. If your build tool already bundles jQuery 3.x via AMD, the jQuery 4.0 upgrade may require build config changes (Webpack/Vite). [src7]
- Vue 3.6 (beta as of late 2025/2026) introduces **Vapor Mode** — a compile-time strategy that eliminates the virtual DOM for opted-in components, reaching Solid/Svelte-class performance. It is 100% opt-in (`<script setup vapor>`, `createVaporApp`, `vaporInteropPlugin` for mixing with vDOM components) and deliberately omits the Options API, global properties, and `getCurrentInstance()`. It is feature-complete but still unstable; target Vue 3.5 for production jQuery migrations and adopt Vapor Mode only for isolated new components. [src9]
- Vue 3.6 also refactors `@vue/reactivity` on top of alien-signals, further improving reactivity throughput and memory use over the 3.5 baseline. This is transparent to migration code — `ref`/`reactive`/`computed` APIs are unchanged. [src9]

## Related Units

- [jQuery to React Migration](/software/migrations/jquery-to-react/2026)
- [jQuery to Vanilla JavaScript](/software/migrations/jquery-to-vanilla-js/2026)
- [JavaScript to TypeScript Migration](/software/migrations/javascript-to-typescript/2026)
- [Webpack to Vite Migration](/software/migrations/webpack-to-vite/2026)
