Refactor patterns

Migration playbook

jQuery Migration Tips and Refactor Patterns

Use these patterns when you want smaller, reversible steps instead of a full rewrite. Each section turns a common migration concern into an implementation pattern you can copy.

  • Audit first

    Capture selectors, events, and state writes before you move a line of code.

  • Refactor in slices

    Replace one behavior at a time, then validate it before touching the next dependency.

Open the guide

Keep the reference page open alongside this guide when you need the exact DOM API equivalent for a helper you are removing.

Search and filter

Browse every section, filter by category, or press / to search instantly.

Filter by guide category

Open reference page

A successful jQuery migration is less about rewriting everything and more about shrinking uncertainty one behavior at a time.

Use this page when the reference tells you which native API to use, but you still need a safe pattern for applying it inside an existing codebase.

Who this helps

Teams modernising legacy UI without stopping feature work or re-platforming the whole frontend.

How to use it

Pick one risk area, apply the pattern, validate it in production-like conditions, then move to the next slice.

Goal

Make each migration step explicit enough that code review, QA, and rollback remain straightforward.

Audit and plan

Treat every removal of jQuery as a behavior audit first. Capture the existing contract, then refactor the implementation behind that contract.

Inventory selectors before you refactor

Before changing an implementation, list the selectors, delegated events, data attributes, and stateful class names a component already relies on. This gives you a concrete definition of “done” for the refactor.

Capture the contract

const navContract = {
  root: '#sidebar',
  selectors: ['.nav-link', '[data-section-id]'],
  delegatedEvents: ['click', 'keydown'],
  reads: ['dataset.sectionId', 'textContent'],
  writes: ['classList', 'aria-expanded'],
};
See more in the platform docs: Use data attributes

Common refactor patterns

Once the behavior is mapped, move the code toward smaller named steps. The browser APIs are already there; the main work is making the flow obvious.

Replace chained selections with explicit steps

Long jQuery chains hide collection boundaries. Split them into named arrays so the query, the filtering logic, and the mutation are easy to inspect independently.

Before (jQuery)
$('#sidebar .nav-link')
  .not('.is-disabled')
  .slice(0, 4)
  .addClass('is-ready');
After (vanilla JS)
const readyLinks = Array.from(document.querySelectorAll('#sidebar .nav-link'))
  .filter((link) => !link.classList.contains('is-disabled'))
  .slice(0, 4);

readyLinks.forEach((link) => {
  link.classList.add('is-ready');
});

Move repeated DOM work into named helpers

When several handlers touch the same DOM state, move that state transition into one helper. Named helpers are easier to review than repeated chains spread across multiple callbacks.

function setPanelExpanded(panel, isExpanded) {
  panel.classList.toggle('is-open', isExpanded);
  panel.setAttribute('aria-hidden', String(!isExpanded));
}

function setButtonExpanded(button, isExpanded) {
  button.classList.toggle('is-active', isExpanded);
  button.setAttribute('aria-expanded', String(isExpanded));
}
See more in the DOM API docs: Element.classList

Convert delegated events with closest()

Most delegated jQuery handlers map cleanly to one native listener on the container plus a closest() guard for the actionable element.

Before (jQuery)
$('#results').on('click', '.remove-button', function () {
  $(this).closest('.result').remove();
});
After (vanilla JS)
const results = document.querySelector('#results');

results?.addEventListener('click', (event) => {
  const target = event.target;
  if (!(target instanceof HTMLElement)) return;

  const button = target.closest('.remove-button');
  if (!button) return;

  button.closest('.result')?.remove();
});
See more in the DOM API docs: Element.closest()

Swap $.ajax for fetch with explicit errors

The native fetch() API is usually enough, but it is more explicit: you check response.ok, decide how to parse the body, and surface errors intentionally.

Before (jQuery)
$.ajax({
  url: '/api/profile',
  method: 'GET',
  dataType: 'json',
  success(data) {
    renderProfile(data);
  },
  error() {
    showError();
  },
});
After (vanilla JS)
async function loadProfile() {
  const response = await fetch('/api/profile');

  if (!response.ok) {
    throw new Error(`Profile request failed: ${response.status}`);
  }

  const data = await response.json();
  renderProfile(data);
}

loadProfile().catch(showError);
See more in the web platform docs: Using Fetch

Use small adapters during incremental rewrites

If a migration spans many files, a tiny adapter layer can reduce churn. Keep it narrow and temporary so the team does not just rebuild jQuery under a different name.

export const dom = {
  qs: (selector, scope = document) => scope.querySelector(selector),
  qsa: (selector, scope = document) => Array.from(scope.querySelectorAll(selector)),
  on: (target, type, handler, options) => target.addEventListener(type, handler, options),
};

Adapters work best when they encode your team’s conventions, not when they try to preserve every legacy chain exactly.

Release checks

The last step is operational: verify the refactor with the same discipline you would apply to a feature release.

Ship each refactor with a migration checklist

A migration is only complete when behavior, accessibility, and cleanup all survive review. Keep the checklist short enough that teams will actually use it.

Migration checklist
- selectors still match the rendered HTML
- delegated events still work for dynamic content
- aria-* state matches visual state
- removed jQuery import is no longer needed
- regression test or manual smoke test was updated
See more in the DOM events docs: DOM events reference