createRoot(), replace jQuery patterns with React state and hooks, then remove jQuery when no code depends on it. [src1, src3]createRoot(document.getElementById('react-mount')).render(<App />)$(el).html() with React state causes desync bugs that are hard to trace. [src1]createRoot() from react-dom/client — the legacy ReactDOM.render() was removed in React 19. All migration code must use the modern API. [src2, src7]useEffect with a cleanup function that calls $el.off() and $el.plugin('destroy'). [src1]$.Deferred is not Promise/A+ compliant — convert all deferred-based AJAX to native fetch + async/await before integrating with React state. [src5]root.unmount() first to prevent detached DOM trees. [src2]| jQuery Pattern | React Equivalent | Example |
|---|---|---|
$('#el').html(val) | useState + JSX | const [text, setText] = useState('') → <p>{text}</p> |
$('#el').on('click', fn) | JSX event handler | <button onClick={handleClick}> |
$('#el').toggle() | Conditional rendering | {show && <Modal />} |
$('#el').addClass('active') | Dynamic className | <div className={active ? 'active' : ''}> |
$('#el').css({color: 'red'}) | Inline style object | <div style={{color: 'red'}}> |
$('#el').val() | Controlled input | <input value={val} onChange={e => setVal(e.target.value)} /> |
$.ajax({url, success}) | fetch + useEffect | useEffect(() => { fetch(url).then(...) }, []) |
$('#el').animate() | CSS transitions or Framer Motion | transition: opacity 0.3s |
$(document).ready(fn) | useEffect(() => {}, []) | Runs after component mounts |
$.each(arr, fn) | arr.map(item => <Li />) | {items.map(i => <Item key={i.id} {...i} />)} |
$('#el').find('.child') | useRef + child refs | ref.current.querySelector(...) |
$('#form').serialize() | FormData or controlled state | new FormData(formRef.current) |
$('#el').show() / .hide() | State-driven rendering | {isVisible && <Component />} |
$.Deferred() | Promise / async/await | const data = await fetch(url).then(r => r.json()) |
$('#plugin').pluginName() | useEffect + useRef wrapper | See jQuery Plugin Wrapper pattern |
START
├── Is this a full rewrite or incremental migration?
│ ├── FULL REWRITE → Set up new React project (Vite/Next.js), rebuild from scratch
│ └── INCREMENTAL (strangler fig) ↓
├── How large is the jQuery codebase?
│ ├── SMALL (<5K LOC) → Migrate all at once over 1-2 sprints
│ └── MEDIUM/LARGE (>5K LOC) ↓
├── Does the page have isolated UI widgets (modals, tabs, forms)?
│ ├── YES → Start with "React Islands": mount React into existing page via createRoot()
│ └── NO ↓
├── Does the jQuery code heavily mutate shared global state?
│ ├── YES → First extract state into a shared store (Zustand/Context), then migrate UI
│ └── NO ↓
├── Are there jQuery plugins with no React equivalent?
│ ├── YES → Wrap them in a React component using useEffect + useRef
│ └── NO ↓
├── Does the app need server-side rendering?
│ ├── YES → Use Next.js as the migration target, migrate page-by-page
│ └── NO ↓
└── DEFAULT → Migrate page-by-page: replace jQuery selectors with React components
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
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
# List jQuery plugins used
grep -rn '\.pluginName\|\.datepicker\|\.chosen\|\.select2\|\.slick\|\.modal' --include='*.js' | sort -u
Verify: Review the counts — a typical medium app has 50–200 jQuery call sites. Prioritize by page traffic.
Install React into your existing project without removing jQuery. Both can coexist on the same page. [src1, src2]
npm install react react-dom
# If using Vite:
npm create vite@latest react-app -- --template react
# If adding to existing Webpack:
npm install @babel/preset-react --save-dev
Verify: import { createRoot } from 'react-dom/client' compiles without errors.
Add empty <div> containers where React components will render. jQuery continues to manage the rest. Each React “island” gets its own createRoot() call. [src1, src6]
import { createRoot } from 'react-dom/client';
import SearchWidget from './components/SearchWidget';
import Sidebar from './components/Sidebar';
// Multiple roots on the same page is fully supported [src2]
const searchContainer = document.getElementById('react-search-widget');
if (searchContainer) {
createRoot(searchContainer).render(<SearchWidget />);
}
const sidebarContainer = document.getElementById('react-sidebar');
if (sidebarContainer) {
createRoot(sidebarContainer).render(<Sidebar />);
}
Verify: React components render within the existing page without breaking jQuery functionality.
Convert imperative DOM updates to declarative React state. Stop reading from the DOM and start reading from state. [src5, src8]
// BEFORE: jQuery — DOM is the state
$('#counter').text(count);
$('#increment').on('click', function() {
count++;
$('#counter').text(count);
});
// AFTER: React — state drives the UI
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
Verify: Remove the jQuery code for that widget, confirm the React version behaves identically.
Convert AJAX calls to the modern fetch API with React hooks. Always include AbortController for cleanup. [src1, src5]
function UserList() {
const [users, setUsers] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
fetch('/api/users', { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => setUsers(data))
.catch(err => {
if (err.name !== 'AbortError') setError(err.message);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Verify: Network tab shows the same API requests as the old jQuery version.
For plugins without React alternatives, wrap them using useRef and useEffect with cleanup. React owns the component lifecycle; jQuery owns only the plugin DOM inside the ref container. [src1]
function ChosenSelect({ options, value, onChange }) {
const selectRef = useRef(null);
useEffect(() => {
const $el = $(selectRef.current);
$el.chosen({ width: '100%' });
const handleChange = (e) => onChange(e.target.value);
$el.on('change', handleChange);
return () => {
$el.off('change', handleChange);
$el.chosen('destroy');
};
}, []);
useEffect(() => {
$(selectRef.current).val(value).trigger('chosen:updated');
}, [value]);
return (
<select ref={selectRef} value={value}>
{options.map(opt =>
<option key={opt.value} value={opt.value}>{opt.label}</option>
)}
</select>
);
}
Verify: Plugin initializes on mount, updates on prop changes, cleans up on unmount — no console errors or memory leaks.
Before migrating pages, extract common UI patterns into reusable React components. This prevents each page from reimplementing elements with inconsistent styles. [src3, src4]
// components/Button.jsx
function Button({ variant = 'primary', size = 'md', children, ...props }) {
return (
<button className={`btn btn-${variant} btn-${size}`} {...props}>
{children}
</button>
);
}
// components/Modal.jsx — replaces $.fn.modal
function Modal({ isOpen, onClose, title, children }) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<h2>{title}</h2>
{children}
<button onClick={onClose}>Close</button>
</div>
</div>
);
}
Verify: Import components into two different pages; confirm consistent styling and behavior.
Once all components are migrated, uninstall jQuery and remove script tags. [src3, src4]
npm uninstall jquery
grep -rn '\$(\|jQuery(' --include='*.js' --include='*.tsx' --include='*.jsx'
# Verify bundle no longer includes jQuery
npx vite build && du -h dist/assets/*.js | sort -rh | head -5
Verify: grep -rn 'jquery' --include='*.json' --include='*.js' returns zero results. App runs without jQuery loaded.
Full script: javascript-react-full-page-migration-from-jquery-t.js (47 lines)
// Input: A jQuery page with search form, results list, and loading spinner
// Output: Equivalent React component with hooks
import { useState, useCallback } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleSearch = useCallback(async (e) => {
e.preventDefault();
if (!query.trim()) return;
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
setResults(data.items);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [query]);
return (
<div>
<form onSubmit={handleSearch}>
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." />
<button type="submit" disabled={loading}>
{loading ? 'Searching...' : 'Search'}
</button>
</form>
{error && <p className="error">{error}</p>}
<ul>{results.map(item => <li key={item.id}>{item.title}</li>)}</ul>
</div>
);
}
export default SearchPage;
Full script: typescript-react-bridging-jquery-and-react-during-.ts (33 lines)
// Input: A legacy jQuery page where React needs to read/write shared state
// Output: A bridge utility that lets jQuery and React share data
import { useSyncExternalStore } from 'react';
class BridgeStore<T> {
private state: T;
private listeners = new Set<() => void>();
constructor(initialState: T) { this.state = initialState; }
getState = (): T => this.state;
setState(partial: Partial<T>): void {
this.state = { ...this.state, ...partial };
this.listeners.forEach(l => l());
}
subscribe = (listener: () => void): (() => void) => {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
};
}
// jQuery: window.appStore = new BridgeStore({ user: null });
// jQuery: window.appStore.setState({ user: { name: 'Alice' } });
function useBridgeStore<T>(store: BridgeStore<T>): T {
return useSyncExternalStore(store.subscribe, store.getState);
}
Full script: python-flask-migrating-server-rendered-jquery-page.py (25 lines)
# Input: Flask app that renders HTML with inline jQuery
# Output: Same app refactored to serve React SPA with JSON API
from flask import Flask, jsonify, send_from_directory
from flask_cors import CORS
app = Flask(__name__, static_folder='react-app/dist')
CORS(app)
@app.route('/api/users')
def api_users():
users = db.get_users()
return jsonify([{'id': u.id, 'name': u.name} for u in users])
# Serve React SPA for all non-API routes
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve_react(path):
if path and (app.static_folder / path).exists():
return send_from_directory(app.static_folder, path)
return send_from_directory(app.static_folder, 'index.html')
// Input: jQuery tab panel that dynamically removes/adds DOM nodes
// Output: React roots that properly mount/unmount with jQuery lifecycle [src2]
const roots = new Map();
function mountReactInTab(tabId, Component) {
const container = document.getElementById(`react-${tabId}`);
if (!container) return;
const root = createRoot(container);
root.render(<Component />);
roots.set(tabId, root);
}
function unmountReactInTab(tabId) {
const root = roots.get(tabId);
if (root) {
root.unmount();
roots.delete(tabId);
}
}
// jQuery tab change handler
$('#tabs').on('tabchange', function(e, { deactivated, activated }) {
unmountReactInTab(deactivated);
mountReactInTab(activated, tabComponents[activated]);
});
// BAD — jQuery DOM manipulation inside a React component
function Counter() {
const handleClick = () => {
const current = parseInt($('#count').text());
$('#count').text(current + 1);
};
return (
<div>
<span id="count">0</span>
<button onClick={handleClick}>+1</button>
</div>
);
}
// GOOD — React state drives the UI
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
// BAD — jQuery event delegation in React
useEffect(() => {
$(document).on('click', '.list-item', function() {
$(this).toggleClass('selected');
});
}, []);
// GOOD — React handles events and state
function ListItem({ item }) {
const [selected, setSelected] = useState(false);
return (
<li className={selected ? 'list-item selected' : 'list-item'}
onClick={() => setSelected(s => !s)}>
{item.name}
</li>
);
}
// BAD — No cancellation, state update on unmounted component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
$.ajax({ url: `/api/users/${userId}`, success: (data) => setUser(data) });
}, [userId]);
return <div>{user?.name}</div>;
}
// GOOD — Proper cleanup prevents memory leaks
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(data => setUser(data))
.catch(err => { if (err.name !== 'AbortError') console.error(err); });
return () => controller.abort();
}, [userId]);
return <div>{user?.name}</div>;
}
// BAD — Loading an entire jQuery page inside React
function LegacyPage() {
useEffect(() => {
$('#legacy-container').load('/old-page.html', function() {
initAllPlugins();
});
}, []);
return <div id="legacy-container" />;
}
// GOOD — Replace one widget at a time
const container = document.getElementById('search-mount');
if (container) {
createRoot(container).render(<SearchWidget />);
}
// Rest of the page is still managed by jQuery
// BAD — Each page reimplements UI from scratch
// Page1: <button style={{background: 'blue'}}>Save</button>
// Page2: <button style={{background: '#0066ff'}}>Submit</button>
// GOOD — Shared component library
function Button({ variant = 'primary', children, ...props }) {
return <button className={`btn btn-${variant}`} {...props}>{children}</button>;
}
// All pages: <Button>Save</Button>
// BAD — jQuery removes DOM with active React root inside
$('#tabs').on('hide', function(e) {
$(e.target).find('.tab-content').remove(); // React root orphaned!
});
// GOOD — Clean up React root first [src2]
$('#tabs').on('hide', function(e) {
const tabId = $(e.target).data('tab-id');
const root = reactRoots.get(tabId);
if (root) {
root.unmount();
reactRoots.delete(tabId);
}
$(e.target).find('.tab-content').remove();
});
return () => { $el.off(); $el.plugin('destroy'); }. [src1]createRoot directly in your entry point — React handles its own mounting lifecycle. [src5]AbortController with fetch or return a cancellation flag in useEffect cleanup. [src1, src5]forwardRef — ref is now a regular prop. If your jQuery plugin wrappers use forwardRef, plan to update them. Fix: Pass ref directly as a prop; codemod available: npx codemod react/19/remove-forward-ref. [src7]# Count remaining jQuery references in the codebase
grep -rn '\$(\|jQuery\|\.ajax\|\.on(' --include='*.js' --include='*.jsx' --include='*.tsx' | wc -l
# Find jQuery script tags in HTML files
grep -rn 'jquery\|jQuery' --include='*.html' | grep -i 'script'
# Check bundle size for jQuery (Webpack)
npx webpack --stats --json | jq '.assets[] | select(.name | contains("jquery"))'
# List all React mount points
grep -rn 'createRoot' --include='*.js' --include='*.jsx' --include='*.tsx' | wc -l
# Find components still importing jQuery
grep -rn "import.*jquery\|require.*jquery" --include='*.js' --include='*.jsx' --include='*.tsx'
# Verify React is rendering (browser console)
# document.querySelectorAll('[data-reactroot]').length
# Check for memory leaks from jQuery plugins (Chrome DevTools)
# Performance tab → Record → Interact → Check heap snapshots
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| React 19 (2024-12) | Current | forwardRef deprecated, ReactDOM.render removed | Use ref as regular prop; createRoot exclusively. Codemod: npx codemod react/19/remove-forward-ref |
| React 18 (2022-03) | LTS | ReactDOM.render → createRoot, automatic batching | Replace all ReactDOM.render() calls. Multiple roots fully supported. |
| React 17 (2020-10) | Maintenance | New JSX transform, event delegation to root | No import React needed with new transform |
| React 16 (2017-09) | EOL | Fiber reconciler, Portals | Upgrade path well-documented |
| jQuery 3.7 (2023) | Current | — | Works alongside React; remove only after full migration |
| jQuery 2.x | Deprecated | No IE6-8 support | Upgrade to 3.x before migrating to React |
| jQuery 1.x | EOL | — | Upgrade to 3.x first; 1.x has known XSS vulnerabilities |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building complex SPAs with rich interactive state | Static sites with minimal interactivity | Vanilla JS or Alpine.js |
| Team needs component reusability across pages | Prototype or throwaway project | Keep jQuery |
| App has >10 dynamic UI widgets | jQuery codebase is <500 lines | Incremental vanilla JS refactor |
| Server-side rendering is needed | Server already renders perfect HTML | Hotwire/Turbo, htmx |
| Hiring — React devs are easier to find | Team is productive with jQuery, no new features planned | Maintain as-is |
| Need TypeScript support for large codebases | Budget/timeline doesn’t allow migration | Add TypeScript to jQuery code |
| Performance-critical UI with frequent re-renders | Simple form submissions and page navigation | Keep jQuery or use htmx |
createRoot() instead of the deprecated ReactDOM.render(). React 19 removed ReactDOM.render() entirely. All code examples use the modern API. [src2, src7]jQuery.Deferred which is not Promise/A+ compliant — convert to native Promises or async/await when migrating AJAX calls. [src5]className props in React components.forwardRef. If jQuery plugin wrappers used forwardRef, update them to pass ref as a regular prop. Codemod: npx codemod react/19/remove-forward-ref. [src7]