Rewriting a Legacy jQuery App in React: What I'd Do Differently

By James Nguyen Updated September 24, 2026
Rewriting a Legacy jQuery App in React: What I'd Do Differently

I inherited an internal admin dashboard built in jQuery around 2015, roughly forty thousand lines of DOM manipulation spread across dozens of files, with global state living in variables attached directly to the window object. My manager asked for a "gradual modernization" without a hard deadline, which sounded reasonable until eight months in when I realized my approach had actually made the codebase worse before it got better, and I want to be honest about that instead of pretending it was smooth the whole way.

My first mistake was rewriting the wrong pages first

I started with the pages I personally found most annoying to maintain, which felt satisfying but weren't the pages anyone else on the team actually touched often. Six weeks of rewrite effort went into pages that got maybe two code changes a year, while the genuinely painful, frequently modified inventory management pages stayed in jQuery the entire time, still causing the exact daily friction the rewrite was supposed to fix.

Running React and jQuery in the same page at once

Since a full rewrite wasn't happening at once, I needed React components to coexist with untouched jQuery code on the same page during the transition. I mounted individual React components into specific DOM containers using ReactDOM.createRoot, while leaving surrounding jQuery-controlled markup untouched, effectively treating each React component as an island inside a jQuery ocean.

// legacy page still has jQuery managing most of the DOM
const container = document.getElementById('order-status-widget');
const root = ReactDOM.createRoot(container);
root.render();
// jQuery elsewhere on the page remains completely untouched

The shared state problem nobody warns you about clearly enough

The genuinely hard part wasn't rendering React components inside a jQuery page, it was keeping state synchronized between the two worlds when a jQuery-driven action needed to update something a React component displayed, and vice versa. I built a small custom event bus, plain browser CustomEvent objects, to let jQuery code dispatch events that React components listened for, and let React dispatch events jQuery code could react to, rather than trying to share a single state management system across two completely different paradigms.

// jQuery side, dispatching an event React listens for
$('#save-button').on('click', function() {
  saveOrder();
  document.dispatchEvent(new CustomEvent('orderSaved', { detail: { orderId } }));
});

// React side, listening
useEffect(() => {
  const handler = (e) => refetchOrderStatus(e.detail.orderId);
  document.addEventListener('orderSaved', handler);
  return () => document.removeEventListener('orderSaved', handler);
}, []);

Why I eventually stopped trying to preserve every jQuery plugin

Several pages relied on jQuery UI plugins, a specific date range picker being the worst offender, that had no clean React equivalent behaving identically, and I spent real time early on trying to wrap those plugins inside React component lifecycles rather than accepting that some pieces genuinely needed to be replaced with React-native equivalents instead of preserved. Once I stopped fighting to keep every legacy plugin alive, individual page rewrites went noticeably faster.

Reordering priorities around actual usage data, not personal annoyance

After that first six weeks of low-impact rewrites, I pulled actual page view analytics for the internal tool and reordered the entire remaining rewrite plan around genuine usage frequency, starting with inventory management, which turned out to be viewed nearly fifty times more often per week than the pages I'd started with out of personal preference.

What eight months and one wrong turn actually taught me

A gradual rewrite without a deadline needs an explicit prioritization method from day one, or it defaults to whatever feels most satisfying to the person doing the work, which is exactly the mistake I made. The technical approach, islands of React inside jQuery pages connected by a lightweight event bus, worked well and I'd use it again without hesitation. The planning mistake, rewriting by personal annoyance instead of actual usage data, cost me six real weeks I don't get back, and it's the specific lesson I now bring to every gradual migration since.

Daniel Justin

About the Author

James Nguyen is a full-stack programmer with more than ten years of experience engineering software systems. Specializing in the Node.js and Python ecosystems, he focuses on backend architecture, API design, and clean data integration. Follow me on YouTube and Instagram.

More Articles