---
# === IDENTITY ===
id: software/migrations/jquery-to-react/2026
canonical_question: "How do I migrate a jQuery codebase to React?"
aliases:
  - "jQuery to React migration guide"
  - "convert jQuery to React"
  - "replace jQuery with React"
  - "rewrite jQuery app in React"
  - "modernize jQuery frontend to React"
  - "incremental jQuery to React migration"
  - "jQuery to React component mapping"
  - "strangler fig jQuery to React"
entity_type: software_reference
domain: software > migrations > jquery_to_react
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.90
version: 1.2
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "React 19 (2024-12)"
  next_review: 2026-11-13
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "React 18+ requires createRoot() — ReactDOM.render() was removed in React 19"
  - "Never let jQuery and React manage the same DOM node — isolate each library's DOM subtree"
  - "jQuery plugins must be wrapped in useEffect with cleanup to prevent memory leaks"
  - "jQuery 3.x Deferred is not Promise/A+ compliant — convert to native Promises before migrating AJAX calls"
  - "Call root.unmount() before jQuery removes a container that holds a React root"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User wants to migrate to Vue instead"
    use_instead: "software/migrations/jquery-to-vue/2026"
  - condition: "User wants vanilla JS without a framework"
    use_instead: "software/migrations/jquery-to-vanilla-js/2026"
  - condition: "User wants to migrate between React versions (e.g., class to hooks)"
    use_instead: "software/migrations/react-classes-to-hooks/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: codebase_size
    question: "How large is the jQuery codebase?"
    type: choice
    options: ["Small (<5K LOC)", "Medium (5K-50K LOC)", "Large (>50K LOC)"]
  - key: migration_strategy
    question: "Do you want incremental or full rewrite?"
    type: choice
    options: ["Incremental (strangler fig)", "Full rewrite"]
  - key: jquery_plugins
    question: "Does the app rely on jQuery plugins without React equivalents?"
    type: choice
    options: ["Yes — must wrap plugins", "No — all have React alternatives", "Not sure"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/jquery-to-react/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/migrations/javascript-to-typescript/2026"
      label: "JavaScript to TypeScript Migration"
    - id: "software/migrations/webpack-to-vite/2026"
      label: "Webpack to Vite Migration"
  alternative_to:
    - id: "software/migrations/jquery-to-vue/2026"
      label: "jQuery to Vue Migration"
    - id: "software/migrations/jquery-to-vanilla-js/2026"
      label: "jQuery to Vanilla JavaScript"
  often_confused_with:
    - id: "software/migrations/react-classes-to-hooks/2026"
      label: "React Class Components to Hooks"
  depends_on: []
  solves: []

# === SOURCES (8 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: "Integrating with Other Libraries"
    author: React
    url: https://legacy.reactjs.org/docs/integrating-with-other-libraries.html
    type: official_docs
    published: 2023-03-01
    reliability: authoritative
  - id: src2
    title: "createRoot — React"
    author: React
    url: https://react.dev/reference/react-dom/client/createRoot
    type: official_docs
    published: 2024-01-01
    reliability: authoritative
  - id: src3
    title: "Replacing jQuery with React: a pragmatic migration plan (with real estimates)"
    author: MarsBased
    url: https://marsbased.com/blog/2026/01/08/replacing-jquery-with-react-a-pragmatic-migration-plan-with-real-estimates
    type: technical_blog
    published: 2026-01-08
    reliability: high
  - id: src4
    title: "Key takeaways — Migrating our WebUI from jQuery to React"
    author: Druva
    url: https://www.druva.com/blog/key-takeaways-migrating-our-webui-from-jquery-to-react
    type: technical_blog
    published: 2023-06-15
    reliability: high
  - id: src5
    title: "Migrating jQuery Code to React.js"
    author: Pluralsight
    url: https://www.pluralsight.com/resources/blog/guides/migrating-jquery-code-to-reactjs
    type: community_resource
    published: 2023-01-15
    reliability: moderate_high
  - id: src6
    title: "Migrating Your Front-end To React, Step By Step"
    author: Xebia
    url: https://xebia.com/blog/migrating-to-react-step-by-step/
    type: technical_blog
    published: 2023-08-10
    reliability: moderate_high
  - id: src7
    title: "React 19 Upgrade Guide"
    author: React
    url: https://react.dev/blog/2024/04/25/react-19-upgrade-guide
    type: official_docs
    published: 2024-04-25
    reliability: authoritative
  - id: src8
    title: "From Legacy jQuery to Modern React: A Step-by-Step Migration Guide for EdTech Monoliths"
    author: Om Shree
    url: https://medium.com/@omshree0709/from-legacy-jquery-to-modern-react-a-step-by-step-migration-guide-for-edtech-monoliths-a9cfa1d50a36
    type: technical_blog
    published: 2025-04-15
    reliability: moderate_high
  - id: src9
    title: "Frontend Modernization Without Rewriting Everything (2026)"
    author: AlterSquare
    url: https://altersquare.medium.com/frontend-modernization-without-rewriting-everything-2ff11b6b57b0
    type: technical_blog
    published: 2026-02-10
    reliability: moderate_high
---

# jQuery to React Migration Guide

## TL;DR

- **Bottom line**: Migrate incrementally using the strangler fig pattern — mount React components into jQuery-managed pages one at a time using `createRoot()`, replace jQuery patterns with React state and hooks, then remove jQuery when no code depends on it. [src1, src3]
- **Key tool/command**: `createRoot(document.getElementById('react-mount')).render(<App />)`
- **Watch out for**: Direct DOM manipulation inside React components — mixing `$(el).html()` with React state causes desync bugs that are hard to trace. [src1]
- **Works with**: React 18+/19, any jQuery version (1.x-3.x), Webpack, Vite, or plain script tags.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- React 18+ requires `createRoot()` from `react-dom/client` — the legacy `ReactDOM.render()` was removed in React 19. All migration code must use the modern API. [src2, src7]
- Never let jQuery and React manage the same DOM node. React's virtual DOM reconciliation assumes it owns the subtree. If jQuery mutates a React-managed node, React loses track of state and the UI desyncs silently. [src1]
- jQuery plugins that attach event listeners must be wrapped in `useEffect` with a cleanup function that calls `$el.off()` and `$el.plugin('destroy')`. Skipping cleanup causes memory leaks on component unmount. [src1]
- jQuery 3.x `$.Deferred` is not Promise/A+ compliant — convert all deferred-based AJAX to native `fetch` + `async/await` or wrap in `new Promise()` before integrating with React state. [src5]
- When jQuery removes a DOM container that holds a React root (e.g., tab panels, accordions), call `root.unmount()` first to prevent detached DOM trees and React warnings. [src2]

## Quick Reference

| 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` or `<motion.div>` |
| `$(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 | `const ref = useRef(null)` -> `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 below |

## Decision Tree

```
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 (window.appState, data-* attrs)?
│   ├── 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 (see Code Examples)
│   └── 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, use controlled inputs, replace $.ajax with fetch/useEffect
```

## 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
# 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 migration by page traffic. [src3]

### 2. Set up React alongside jQuery

Install React into your existing project without removing jQuery. Both coexist on the same page. React mounts into specific DOM nodes while jQuery manages everything else. [src1, src2]

```bash
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 in your build pipeline.

### 3. Create mount points in existing HTML

Add empty `<div>` containers where React components will render. jQuery continues to manage the rest of the page. Each React "island" gets its own `createRoot()` call. [src1, src6]

```html
<!-- Existing jQuery page -->
<div id="legacy-header"><!-- jQuery manages this --></div>
<div id="react-search-widget"></div>  <!-- React mounts here -->
<div id="legacy-content"><!-- jQuery manages this --></div>
<div id="react-sidebar"></div>  <!-- Another React island -->
```

```javascript
// Mount React into the existing page
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 layout without breaking jQuery functionality.

### 4. Replace jQuery DOM manipulation with React state

Convert imperative DOM updates to declarative React state. This is the core of the migration: stop reading from the DOM and start reading from state. [src5, src8]

```javascript
// 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 renders and behaves identically.

### 5. Replace $.ajax with fetch + useEffect

Convert AJAX calls to the modern fetch API with React hooks. Centralize API configuration (base URL, headers, auth tokens) into a shared utility. Always include AbortController for cleanup. [src1, src5]

```javascript
// BEFORE: jQuery AJAX
$.ajax({
  url: '/api/users',
  method: 'GET',
  success: function(data) { renderUsers(data); },
  error: function(err) { showError(err); }
});

// AFTER: React with proper cleanup
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. Compare response handling with the old jQuery version.

### 6. Wrap jQuery plugins that have no React equivalent

For plugins without React alternatives (date pickers, rich text editors, charts), wrap them in a React component using `useRef` and `useEffect` with cleanup. The key rule: React owns the component lifecycle, jQuery owns only the plugin DOM inside the ref container. [src1]

```javascript
import { useEffect, useRef } from 'react';
import $ from 'jquery';
import 'chosen-js';

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');
    };
  }, []);

  // Sync React props to jQuery plugin
  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, and cleans up on unmount (no console errors, no memory leaks).

### 7. Build a shared component library

Before migrating pages, extract common UI patterns (buttons, modals, form inputs, cards) into reusable React components. This prevents each page from reimplementing the same elements with inconsistent styles. [src3, src4]

```javascript
// components/Button.jsx — shared across all migrated pages
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.

### 8. Remove jQuery and clean up

Once all components are migrated, uninstall jQuery and remove script tags. Run a final audit to confirm zero remaining references. [src3, src4]

```bash
npm uninstall jquery
# Remove from HTML:
# <script src="jquery.min.js"></script>  <- delete this
# Search for any remaining $ or jQuery references
grep -rn '\$(\|jQuery(' --include='*.js' --include='*.tsx' --include='*.jsx'
# Check bundle for 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.

## Code Examples

### JavaScript/React: Full page migration from jQuery to React

> Full script: [javascript-react-full-page-migration-from-jquery-t.js](scripts/javascript-react-full-page-migration-from-jquery-t.js) (47 lines)

```javascript
// Input:  A jQuery page with a search form, results list, and loading spinner
// Output: Equivalent React component with hooks
import { useState, useEffect, useCallback } from 'react';
function SearchPage() {
  const [query, setQuery] = useState('');
# ... (see full script)
```

### TypeScript/React: Bridging jQuery and React during migration

> Full script: [typescript-react-bridging-jquery-and-react-during-.ts](scripts/typescript-react-bridging-jquery-and-react-during-.ts) (33 lines)

```typescript
// Input:  A legacy jQuery page where React needs to read/write shared state
// Output: A bridge utility that lets jQuery code and React components share data
import { createRoot, Root } from 'react-dom/client';
import { useState, useEffect, useSyncExternalStore } from 'react';
// Shared store that both jQuery and React can read/write
# ... (see full script)
```

### Python/Flask: Migrating server-rendered jQuery pages to React API

> Full script: [python-flask-migrating-server-rendered-jquery-page.py](scripts/python-flask-migrating-server-rendered-jquery-page.py) (25 lines)

```python
# Input:  A Flask app that renders HTML with inline jQuery
# Output: The same app refactored to serve a React SPA with JSON API endpoints
from flask import Flask, jsonify, send_from_directory
from flask_cors import CORS
app = Flask(__name__, static_folder='react-app/dist')
# ... (see full script)
```

### JavaScript: React root lifecycle management for jQuery tab panels

```javascript
// 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(); // Track active React roots

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();  // Tell React to stop managing this subtree
    roots.delete(tabId);
  }
}

// jQuery tab change handler
$('#tabs').on('tabchange', function(e, { deactivated, activated }) {
  unmountReactInTab(deactivated);
  mountReactInTab(activated, tabComponents[activated]);
});
```

## Anti-Patterns

### Wrong: Direct DOM manipulation inside React components

```javascript
// 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>
  );
}
```

### Correct: Use React state for all UI updates

```javascript
// 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>
  );
}
```

### Wrong: Using jQuery for event delegation in React

```javascript
// BAD — jQuery event delegation alongside React
useEffect(() => {
  $(document).on('click', '.list-item', function() {
    $(this).toggleClass('selected');
  });
}, []);
```

### Correct: Handle events in React with state

```javascript
// 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>
  );
}
```

### Wrong: Using $.ajax inside React without cleanup

```javascript
// BAD — No cancellation, possible 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>;
}
```

### Correct: Use fetch with AbortController cleanup

```javascript
// GOOD — Proper cleanup prevents memory leaks and race conditions
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>;
}
```

### Wrong: Wrapping entire jQuery pages in React

```javascript
// BAD — Embedding a whole jQuery page inside a React component
function LegacyPage() {
  useEffect(() => {
    $('#legacy-container').load('/old-page.html', function() {
      initAllPlugins();  // Re-initializes all jQuery plugins
    });
  }, []);
  return <div id="legacy-container" />;
}
```

### Correct: Migrate granular components, not entire pages

```javascript
// GOOD — Replace one widget at a time with React
function App() {
  return (
    <div>
      {/* React component replaces one jQuery widget */}
      <SearchWidget />
      {/* Rest of the page is still managed by jQuery */}
    </div>
  );
}

// Mount into existing page
const container = document.getElementById('search-mount');
if (container) {
  createRoot(container).render(<App />);
}
```

### Wrong: Skipping component library before page migration

```javascript
// BAD — Each page reimplements buttons, modals, forms from scratch
// Page1.jsx
function Page1() {
  return <button style={{background: 'blue', color: 'white', padding: '8px 16px'}}>Save</button>;
}
// Page2.jsx
function Page2() {
  return <button style={{background: '#0066ff', color: '#fff', padding: '10px 20px'}}>Submit</button>;
}
```

### Correct: Build reusable components first

```javascript
// GOOD — Shared component library before migrating pages
// components/Button.jsx
function Button({ variant = 'primary', children, ...props }) {
  return <button className={`btn btn-${variant}`} {...props}>{children}</button>;
}

// Page1.jsx
function Page1() {
  return <Button>Save</Button>;
}
// Page2.jsx
function Page2() {
  return <Button>Submit</Button>;
}
```

### Wrong: Not unmounting React roots before jQuery removes containers

```javascript
// BAD — jQuery removes DOM with active React root inside
$('#tabs').on('hide', function(e) {
  $(e.target).find('.tab-content').remove(); // React root orphaned!
});
```

### Correct: Unmount React root before jQuery DOM removal

```javascript
// GOOD — Clean up React root first, then jQuery removes the DOM
$('#tabs').on('hide', function(e) {
  const tabId = $(e.target).data('tab-id');
  const root = reactRoots.get(tabId);
  if (root) {
    root.unmount(); // React cleans up first [src2]
    reactRoots.delete(tabId);
  }
  $(e.target).find('.tab-content').remove();
});
```

## Common Pitfalls

- **React and jQuery both managing the same DOM node**: React's virtual DOM and jQuery's direct DOM manipulation conflict, causing UI state to desync. Fix: Give each library its own DOM subtree. React renders into mount points that jQuery never touches. [src1]
- **Forgetting useEffect cleanup for jQuery plugins**: jQuery plugins attach event listeners that persist after React unmounts the component, causing memory leaks. Fix: Always return a cleanup function: `return () => { $el.off(); $el.plugin('destroy'); }`. [src1]
- **Migrating everything at once instead of incrementally**: Big-bang rewrites stall feature development and introduce regression risk. A 50K LOC jQuery app takes 6-12 months for incremental migration vs. 12-18 months for a rewrite with higher failure risk. Fix: Migrate one component/page at a time. Ship each migration to production before starting the next. [src3, src6]
- **Not extracting state from the DOM first**: In jQuery apps, the DOM is the state (hidden inputs, data attributes, element classes). Fix: Before migrating UI, extract this implicit state into JavaScript variables or a shared store (Zustand, Context API). [src4, src8]
- **Using jQuery's $(document).ready() with React**: React components mount asynchronously. Wrapping React rendering in `$(document).ready()` usually works but creates unnecessary coupling. Fix: Use `createRoot` directly in your entry point; React handles its own mounting lifecycle. [src5]
- **Mixing jQuery AJAX with React state without cancellation**: AJAX responses arriving after component unmount cause "Can't perform a React state update on an unmounted component" warnings. Fix: Use `AbortController` with `fetch`, or return a cancellation flag in `useEffect` cleanup. [src1, src5]
- **Ignoring React 19 ref changes**: React 19 deprecated `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; no wrapper needed. [src7]
- **Not tracking migration progress**: Without metrics, migrations stall at 70% completion. Fix: Track jQuery call sites remaining (via grep counts) in CI and set sprint-level targets. [src3]

## Diagnostic Commands

```bash
# 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 (if using Webpack/Vite)
npx webpack --stats --json | jq '.assets[] | select(.name | contains("jquery"))'
# Or with Vite:
npx vite build --reporter=json 2>/dev/null && du -sh dist/assets/*.js

# List all React mount points in the codebase
grep -rn 'createRoot' --include='*.js' --include='*.jsx' --include='*.tsx' | wc -l

# Verify React is rendering correctly (browser console)
# document.querySelectorAll('[data-reactroot]').length

# Find components still importing jQuery
grep -rn "import.*jquery\|require.*jquery" --include='*.js' --include='*.jsx' --include='*.tsx'

# Check for memory leaks from jQuery plugins (Chrome DevTools)
# Performance tab -> Record -> Interact -> Check heap snapshots for detached DOM nodes
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| React 19 (2024-12) | Current | `forwardRef` deprecated (ref is a prop), `ReactDOM.render` removed | Use `ref` as a regular prop; use `createRoot` exclusively. Codemod: `npx codemod react/19/remove-forward-ref` [src7] |
| React 18 (2022-03) | LTS | `ReactDOM.render` -> `createRoot`, automatic batching | Replace all `ReactDOM.render()` calls with `createRoot().render()`. Multiple roots fully supported. [src2] |
| React 17 (2020-10) | Maintenance | New JSX transform, event delegation to root | No `import React` needed in JSX files with new transform |
| React 16 (2017-09) | EOL | Fiber reconciler, Portals | Upgrade path well-documented, minimal breaking changes |
| jQuery 3.7 (2023) | Current | — | Still 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 |

## When to Use / When Not to Use

| 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 (Rails, Django) | Hotwire/Turbo, htmx |
| Hiring: React devs are easier to find | Team is productive with jQuery and no new features planned | Maintain as-is |
| Need TypeScript support for large codebases | Budget/timeline doesn't allow migration | Gradually add TypeScript to jQuery code |
| Performance-critical UI with frequent re-renders | Simple form submissions and page navigation | Keep jQuery or use htmx |

## Important Caveats

- React 18+ requires `createRoot()` instead of the deprecated `ReactDOM.render()`. React 19 removed `ReactDOM.render()` entirely. All code examples in this guide use the modern API. [src2, src7]
- jQuery 3.x uses `jQuery.Deferred` which is not Promise/A+ compliant — convert to native Promises or async/await when migrating AJAX calls. [src5]
- Some jQuery plugins modify DOM nodes that React also manages, which causes reconciliation errors. Always isolate plugin DOM from React-managed DOM using a separate ref container. [src1]
- Bundle size impact: adding React (~44 KB gzipped for react + react-dom) alongside jQuery (~30 KB) temporarily increases page weight by ~74 KB. Remove jQuery as soon as all references are migrated. [src3]
- CSS-in-JS libraries (styled-components, Emotion) are optional — you can keep existing CSS/SCSS and use `className` props in React components.
- React 19 deprecated `forwardRef`. If your jQuery plugin wrappers used `forwardRef`, update them to pass `ref` as a regular prop. A codemod is available: `npx codemod react/19/remove-forward-ref`. [src7]
- During the coexistence period, avoid running React's StrictMode in development for components that wrap jQuery plugins — double-invocation of effects will initialize/destroy plugins twice. [src1]
- For very large jQuery codebases (>100K LOC) with multiple team domains, consider Webpack 5 Module Federation to lazy-load entire React microfrontends at runtime while jQuery still serves the shell. This adds governance complexity but lets independent teams ship React migrations without coordinating a single bundle. [src9]
- 2026 industry data: AI-assisted code analysis (AST-level pattern matching plus LLM code generation) is shortening incremental migrations from ~12 months to ~4–7 months for medium-sized apps. Treat the codemod step as automatable but the architecture-decision step (which widget becomes which component) as still human-led. [src9]

## Related Units

- [jQuery to Vue 3 Migration](/software/migrations/jquery-to-vue/2026)
- [jQuery to Vanilla JavaScript](/software/migrations/jquery-to-vanilla-js/2026)
- [JavaScript to TypeScript Migration](/software/migrations/javascript-to-typescript/2026)
- [React Class Components to Hooks](/software/migrations/react-classes-to-hooks/2026)
- [Webpack to Vite Migration](/software/migrations/webpack-to-vite/2026)
