The self-storage billing dashboards I inherited over the years all had one thing in common: jQuery. Not just a little jQuery — jQuery as the foundation of every interaction, from fetching payment data to validating lease forms to toggling UI state. jQuery made a lot of sense when it was written. IE6 compatibility, inconsistent browser APIs, clunky XHR — it solved real problems. But that code is now running on browsers that have had stable, powerful native APIs for years, and jQuery's 30KB minified payload plus its abstraction overhead is no longer earning its keep.
Migrating billing software UI code is not the same as migrating a marketing site. The stakes are higher. A broken payment form means a tenant can't pay, which means a missed payment that falls on your client's operations staff to sort out. This guide covers the patterns I encounter most in legacy billing dashboards and how I migrate them safely without bringing down production.
Strategy Before Code: The Incremental Approach
The worst migration strategy is ripping out jQuery wholesale and rewriting everything at once. The right approach is running jQuery and vanilla JS side by side temporarily, replacing one pattern at a time, and testing each replacement independently before removing the jQuery dependency. Create a feature flag or a version parameter that lets you serve the migrated code to a test tenant while the legacy code stays live for everyone else.
Start by auditing your jQuery usage. In most billing dashboards, 80% of the jQuery calls are concentrated in a handful of patterns. Identify them before writing a line of replacement code.
AJAX Calls: $.ajax and $.get vs Fetch
This is the most common pattern in billing UIs — loading payment history, fetching invoice data, submitting charges asynchronously. The $.ajax call and its shorthand siblings map cleanly to the Fetch API:
// jQuery — fetching tenant payment history
$.ajax({
url: '/api/payments',
method: 'GET',
data: { tenant_id: tenantId, page: currentPage },
headers: { 'X-Auth-Token': authToken },
success: function(data) { renderPayments(data.payments); },
error: function(xhr) { showError(xhr.responseJSON.message); }
});
// Vanilla — same call with Fetch
const params = new URLSearchParams({ tenant_id: tenantId, page: currentPage });
fetch(`/api/payments?${params}`, {
method: 'GET',
headers: {
'X-Auth-Token': authToken,
'Accept': 'application/json'
}
})
.then(res => {
if (!res.ok) return res.json().then(e => Promise.reject(e));
return res.json();
})
.then(data => renderPayments(data.payments))
.catch(err => showError(err.message));
One gotcha: jQuery's $.ajax rejects the promise on non-2xx status codes automatically. Fetch does not — a 400 or 500 response resolves the promise, it just sets res.ok to false. That explicit if (!res.ok) check is required. Missing it in billing code means a failed charge silently looks like a success to your UI layer.
For POST requests with JSON bodies, which billing forms use constantly:
// jQuery
$.ajax({
url: '/api/charge',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ tenant_id: tenantId, amount: amount }),
success: function(data) { showReceipt(data); }
});
// Vanilla
fetch('/api/charge', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Token': authToken,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ tenant_id: tenantId, amount: amount })
})
.then(res => { if (!res.ok) throw res; return res.json(); })
.then(data => showReceipt(data));
DOM Manipulation: $ Selectors and the Modern DOM
jQuery's selector engine was a revelation in 2006. In 2024, querySelector and querySelectorAll support the same CSS selector syntax natively. The mapping is direct:
// jQuery patterns in billing dashboards
$('#invoice-table').hide();
$('.payment-row').addClass('overdue');
$('#total-amount').text('$' + total.toFixed(2));
$('#submit-btn').prop('disabled', true);
$('.unit-row[data-unit-id="' + unitId + '"]').remove();
// Vanilla equivalents
document.getElementById('invoice-table').style.display = 'none';
document.querySelectorAll('.payment-row').forEach(el => el.classList.add('overdue'));
document.getElementById('total-amount').textContent = '$' + total.toFixed(2);
document.getElementById('submit-btn').disabled = true;
document.querySelector(`.unit-row[data-unit-id="${unitId}"]`)?.remove();
The optional chaining on that last one (?.) is intentional — billing UIs often try to remove rows that may have already been removed by a concurrent update. Null-safe removal prevents the cryptic "Cannot read properties of null" errors that were previously swallowed by jQuery silently doing nothing.
Event Delegation for Dynamic Billing Tables
Billing dashboards render tables dynamically — payment rows, invoice lines, unit listings. jQuery's .on() with a selector argument handles event delegation cleanly, and this is one place where the vanilla replacement requires you to understand what jQuery was actually doing:
// jQuery event delegation — "pay now" buttons in dynamically loaded rows
$('#invoice-table').on('click', '.pay-btn', function() {
var invoiceId = $(this).data('invoice-id');
processPayment(invoiceId);
});
// Vanilla — explicit delegation check
document.getElementById('invoice-table').addEventListener('click', function(e) {
const btn = e.target.closest('.pay-btn');
if (!btn) return;
const invoiceId = btn.dataset.invoiceId;
processPayment(invoiceId);
});
The closest() call handles the case where the click target is a child element inside the button (like an icon). jQuery's delegation did this automatically. Vanilla requires the explicit closest() call — miss it and clicks on icon elements inside buttons will silently do nothing.
Form Validation in Payment Forms
Payment forms in billing software typically validate card token presence, amount ranges, and required fields before submitting to the backend. jQuery Validate was popular for this. The HTML5 Constraint Validation API replaces most of it:
// jQuery Validate pattern
$('#payment-form').validate({
rules: {
amount: { required: true, min: 1, max: 10000 },
note: { maxlength: 255 }
},
submitHandler: function(form) { submitPayment(form); }
});
// Vanilla — Constraint Validation API + custom logic
document.getElementById('payment-form').addEventListener('submit', function(e) {
e.preventDefault();
const form = e.target;
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const amount = parseFloat(form.elements['amount'].value);
if (amount < 1 || amount > 10000) {
form.elements['amount'].setCustomValidity('Amount must be between $1 and $10,000');
form.reportValidity();
return;
}
form.elements['amount'].setCustomValidity('');
submitPayment(form);
});
Add required, min, max, maxlength, and pattern attributes directly on your input elements in the HTML. The browser enforces them. Use setCustomValidity only for business logic that HTML attributes can't express — like cross-field validation or server-side error surfacing.
Removing jQuery Safely
Once you've replaced all jQuery calls in a given module, don't immediately remove the script tag. Run a search for $ and jQuery in your JS files for that module. Check browser devtools Network tab to confirm jQuery is still loading (it may be referenced in a template you haven't touched). Only remove the script tag after a full test cycle covering every user flow in that module — payment submission, invoice generation, unit search, report export.
The final step is removing jQuery from your asset bundle. Run your page through Lighthouse before and after — the reduction in JavaScript parse time on mobile devices is measurable and directly improves tenant portal experience on older phones, which is the primary device class for self-storage tenants managing payments.
What to Expect
A complete migration on a mid-sized billing dashboard typically takes two to four weeks of incremental work when done carefully. The payoff is a smaller, faster, more maintainable codebase with zero dependency on a library that's in maintenance mode. Every developer who comes after you can read the browser's own API documentation to understand what the code does — no jQuery-specific knowledge required. For billing software that needs to be maintained for years, that's a real long-term asset.