createApp().mount(), replace jQuery patterns one widget at a time with reactive state and directives, then remove jQuery when no code depends on it.createApp({ setup() { ... } }).mount('#vue-mount')$(el).html() with Vue's reactivity system causes the virtual DOM to desync from the real DOM.vue.global.js or vue.esm-browser.js) when using in-DOM templates during incremental migration — the runtime-only build excludes the template compiler.unmounted/onUnmounted lifecycle hook to prevent memory leaks and ghost DOM elements.v-html to inject jQuery-rendered HTML — it bypasses Vue reactivity and creates XSS vulnerabilities.$.Deferred, use the full jQuery 4.0 build or migrate to native Promises before switching to Vue composables. [src7]| 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) { ... } }) |
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
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]
# 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.
Install Vue 3 into your existing project without removing jQuery. Both libraries can coexist because Vue only manages DOM inside its mount container. [src1]
# Option A: npm install (for bundler-based projects)
npm install vue@3
# Option B: CDN (for no-build-step projects)
# <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.
Add container elements where Vue will take over. jQuery continues managing everything else. Vue 3 supports multiple independent app instances on one page. [src1, src4]
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.
Convert imperative DOM updates to declarative Vue state using ref() and reactive(). 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]
// 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>
`
};
Verify: Remove the jQuery code for that widget, confirm the Vue version renders and behaves identically. Check Vue DevTools — state should be reactive.
Convert AJAX calls to the modern fetch API wrapped in a reusable composable. Composables are Vue's equivalent of React hooks. Use onWatcherCleanup() (Vue 3.5+) for automatic request cancellation inside watchers. [src3, src6]
// composables/useFetch.js
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 };
}
Verify: Network tab shows the same API requests. Unmounting the component should abort in-flight requests.
For plugins without Vue alternatives, wrap them in a Vue custom directive using the mounted and unmounted lifecycle hooks. [src5, src8]
// directives/v-chosen.js
import $ from 'jquery';
import 'chosen-js';
export const vChosen = {
mounted(el, binding) {
const $el = $(el);
$el.chosen({ width: '100%', ...binding.value });
$el.on('change', () => {
el.dispatchEvent(new Event('change'));
});
},
updated(el) {
$(el).trigger('chosen:updated');
},
unmounted(el) {
$(el).chosen('destroy');
}
};
// Register: app.directive('chosen', vChosen);
// Use: <select v-model="val" v-chosen="{ search_contains: true }">
Verify: Plugin initializes on mount, updates when Vue state changes, and destroys cleanly on unmount — no console errors or memory leaks.
Once all components are migrated, uninstall jQuery 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]
npm uninstall jquery
# Remove from HTML: <script src="jquery.min.js"></script>
grep -rn '\$(\|jQuery(' --include='*.js' --include='*.vue' --include='*.html'
Verify: grep -rn 'jquery' --include='*.json' --include='*.js' --include='*.vue' returns zero results. App runs without jQuery loaded. Bundle size should decrease by ~19.5–30 KB (gzipped).
Full script: javascript-vue-3-full-page-migration-from-jquery-t.js (46 lines)
// Input: A jQuery page with 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';
const query = ref('');
const results = ref([]);
const loading = ref(false);
const error = ref(null);
const hasResults = computed(() => results.value.length > 0);
async function handleSearch() {
if (!query.value.trim()) return;
loading.value = true;
error.value = null;
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query.value)}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
results.value = data.items;
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}
// </script>
Full script: javascript-vue-3-bridge-store-for-jquery-vue-coexi.js (31 lines)
// Input: A legacy jQuery page where Vue needs to read/write shared state
// Output: A reactive bridge that lets jQuery and Vue components share data
// bridge-store.js
import { reactive, watch } from 'vue';
export const bridgeStore = reactive({
user: null,
theme: 'light',
notifications: []
});
// jQuery side: window.bridgeStore = bridgeStore;
// Vue side: already reactive — import and use directly
watch(
() => bridgeStore.theme,
(newTheme) => {
document.body.setAttribute('data-theme', newTheme);
$(document).trigger('theme:changed', [newTheme]);
}
);
Full script: typescript-vue-3-type-safe-composable-replacing-jq.ts (51 lines)
// Input: jQuery $.ajax calls with consistent error handling
// Output: Type-safe Vue 3 composable with automatic loading/error state
// composables/useApi.ts
import { ref, type Ref } from 'vue';
interface UseApiReturn<T> {
data: Ref<T | null>;
error: Ref<string | null>;
loading: Ref<boolean>;
execute: (...args: unknown[]) => Promise<void>;
}
export function useApi<T>(
url: string | ((...args: unknown[]) => string),
options: RequestInit = {}
): UseApiReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>;
const error = ref<string | null>(null);
const loading = ref(false);
// ... (see full script for complete implementation)
}
// ❌ 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>
`
};
// ✅ 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>
`
};
// ❌ 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>`
};
// ✅ 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>
`
};
// ❌ BAD — No cancellation, 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>`
};
// ✅ 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>`
};
// ❌ BAD — Wrapping an entire jQuery page in one Vue app
const app = createApp({
mounted() {
initDatePickers();
initModals();
initDataTables();
initCharts();
},
template: `<div id="entire-page" v-html="legacyHtml"></div>`
});
// ✅ GOOD — Replace one widget at a time with its own Vue app
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
// ❌ BAD — Treating refs as jQuery-style DOM query shortcuts
const FormComponent = {
mounted() {
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>
`
};
// ✅ 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>
`
};
unmounted or onUnmounted: $(el).plugin('destroy'). [src5, src8]data-* attributes, element classes. Fix: Before migrating UI, extract this implicit state into JavaScript variables, a Pinia store, or a shared reactive object. [src4]v-html to inject jQuery-rendered HTML: Bypasses Vue's reactivity system and is an XSS risk. Fix: Convert jQuery-generated HTML to Vue templates with proper data binding. Sanitize with DOMPurify if raw HTML is unavoidable. [src2]$(document).ready() with Vue mounting: Creates unnecessary coupling and race conditions. 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]const promise = new Promise((resolve, reject) => { /* ... */ }). [src7]# 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
grep -rn 'jQuery\.isArray\|jQuery\.parseJSON\|jQuery\.trim\|jQuery\.type\|jQuery\.now' --include='*.js' | wc -l
# Check bundle size for jQuery (Vite)
npx vite build 2>&1 | grep -i jquery
# Verify Vue is rendering (browser console)
# document.querySelectorAll('[data-v-app]').length
# List all mounted Vue app instances (browser console)
# document.querySelectorAll('.__vue_app__')
# Check for memory leaks (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
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Vue 3.5 (Sep 2024) | Current | Reactive props destructure stable, useTemplateRef(), onWatcherCleanup() | Recommended 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 removes Deferreds; ES modules | Can coexist with Vue 3. Check upgrade guide. [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 |
| 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 |
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.$.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]<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 GSAP for complex animations.createApp().mount() unless the app is stored in a variable accessible from the page. During migration, keep app references for debugging: window.__vueApps = { search: searchApp }.