Von jQuery zu React ohne Rewrite: Ein inkrementeller Weg zur Frontend-Modernisierung
Die Migration von jQuery zu React bringt die meisten Teams schon aus dem Tritt, bevor sie eine einzige Zeile neuen Code geschrieben haben. Der übliche Reflex: das alte Frontend für tot erklären, ein frisches React-Projekt aufsetzen und alles neu bauen. Dieser Reflex ist fast immer falsch.
Ein kompletter Rewrite legt die Feature-Entwicklung für Monate still, zwingt dein Team, während der Umstellung zwei parallele Codebasen zu pflegen, und hat eine Ausfallrate, die die meisten Engineering-Manager still und leise unterschätzen. Die jQuery-Anwendung bedient heute echte Nutzer. Der React-Nachbau existiert noch gar nicht.
Es gibt einen besseren Weg: Insel für Insel migrieren, die bestehende Anwendung am Laufen halten und React Komponente für Komponente einführen, bis die alte Codebasis verblasst, statt herausgerissen zu werden.
Dieser Leitfaden geht diesen Weg mit genau den praktischen Details durch, die generische Modernisierungsratschläge gerne auslassen.
Warum der Big-Bang-Rewrite scheitert
Bevor der inkrementelle Ansatz Sinn ergibt, hilft es zu verstehen, warum die Alternative so verlässlich scheitert.
Eine jQuery-Anwendung, die seit drei oder mehr Jahren in Produktion läuft, sammelt implizites Wissen an. Event-Handler, die serverseitige Eigenheiten ausgleichen. DOM-Manipulation, die Sonderfälle im Datenmodell abbildet. Inline-$.ajax-Aufrufe, die nach Produktionsvorfällen nachjustiert wurden. Nichts davon steht in einem Anforderungsdokument.
Wenn ein Team von Null neu schreibt, baut es den Happy Path mit Zuversicht nach und entdeckt die Sonderfälle sechs Monate nach dem Go-Live, wenn die erste Welle echter Nutzer auf das neue Frontend trifft. Genau dann merkt das Business, dass der Rewrite das Doppelte der Schätzung gekostet und die Hälfte dessen ausgeliefert hat, was die ursprüngliche Anwendung konnte.
Der inkrementelle Weg vermeidet das, indem er nie ein funktionierendes Stück jQuery entfernt, bevor ein funktionierendes Stück React es unter Produktionsbedingungen ersetzt hat.
Das Grundprinzip: React-Inseln
Eine React-Insel ist ein in sich geschlossener React-Komponentenbaum, der in einen dafür vorgesehenen DOM-Knoten eingehängt wird, den die bestehende jQuery-Seite besitzt. jQuery kontrolliert weiterhin das umgebende Layout und die Navigation. React besitzt ausschließlich seinen zugewiesenen Container.
Das Muster hat zwei Voraussetzungen:
- Die Host-Seite stellt einen Mount-Point bereit (ein
<div id="react-island-xyz"></div>, das jQuery nicht anfasst). - Die React-Komponente erhält ihre Initialdaten als Props oder von einem sauberen API-Endpunkt, statt aus dem jQuery-verwalteten DOM zu lesen.
Beide Voraussetzungen sind leicht zu erfüllen und erzwingen eine nützliche Disziplin: Die React-Insel kann sich nicht mit der Legacy-DOM-Schicht verheddern. Diese Trennung macht jede Insel unabhängig testbar und unabhängig deploybar.
Das Mounting sieht in seiner einfachsten Form so aus:
// Vanilla-JS-Einstiegspunkt, geladen nachdem jQuery die Seite gerendert hat
import { createRoot } from 'react-dom/client';
import { NotificationCenter } from './components/NotificationCenter';
const el = document.getElementById('react-notification-center');
if (el) {
const root = createRoot(el);
root.render(<NotificationCenter userId={window.__USER_ID__} />);
}
Keine Framework-Integration nötig. Kein Next.js. Keine Generalüberholung des Build-Systems am ersten Tag. Nur ein Bundler, der ein Script-Tag produziert, das dein bestehendes Backend in die Seite einfügt.
Schritt 1: Ein Build-Tool wählen, das neben jQuery funktioniert
Deine Legacy-Anwendung hat vermutlich überhaupt keine JavaScript-Build-Pipeline, oder eine aus einer anderen Ära. Der erste praktische Schritt ist, einen modernen Bundler hinzuzufügen, ohne das bestehende Frontend zu stören.
Vite ist derzeit die Standardwahl. Es startet in unter einer Sekunde, versteht JSX out of the box und erzeugt modulbasierte Ausgabe, die sauber neben Legacy-Skripten lädt.
Lege ein frontend/-Verzeichnis neben deinem bestehenden public/js/-Ordner an. Richte Vite darauf aus. Lass die Legacy-Skripte exakt dort, wo sie sind. Beide Skript-Sätze koexistieren konfliktfrei auf denselben Seiten, solange du darauf achtest, keine doppelten globalen Event-Listener zu registrieren.
Dieser Schritt dauert einen halben Tag. Er macht nichts kaputt. Erledige ihn, bevor du eine einzige jQuery-Datei anfasst.
Schritt 2: Die erste Insel identifizieren
Widerstehe der Versuchung, zuerst das komplexeste Widget zu migrieren. Fang mit etwas an, das wirklich in sich geschlossen ist und das dein Team vollständig versteht: ein Benachrichtigungs-Dropdown, ein Date Picker, ein Datei-Upload-Control, ein Einstellungsformular.
Die Kriterien für eine gute erste Insel:
- Minimale Lese- und Schreibkopplung zum Rest der Seite
- Klarer API-Endpunkt, der die Daten liefern kann (oder innerhalb eines Sprints entstehen kann)
- Sichtbar genug, dass das Team es in Produktion arbeiten sieht
- Isoliert genug, dass ein Bug nicht die umgebende Seite zerlegt
Ein Notification Center ist oft der ideale erste Kandidat. Es liest von einer API, rendert seine eigene UI und beeinflusst den umgebenden Seitenzustand nicht. Ein Formular, das absendet und einen kompletten serverseitigen Seiten-Reload auslöst, ist die schlechtere erste Wahl, weil das Erfolgsverhalten außerhalb der Insel liegt.
Wenn die Seite, an die dein Team denkt, keine saubere API im Rücken hat, baue zuerst den API-Endpunkt. Versuche nicht, die UI zu migrieren und gleichzeitig die Datenschicht herauszulösen. Das sind zwei Migrationen auf einmal und die Ursache der meisten gescheiterten Inseln.
Schritt 3: Datenbeschaffung vor der UI migrieren
Dieser Schritt ist für Nutzer unsichtbar, eliminiert aber die Kategorie von Fehlern, die entsteht, wenn React Zustand direkt aus dem DOM liest.
Prüfe den jQuery-Code der ersten Insel. Jeder $.ajax-Aufruf, jedes $(selector).text(), das servergerenderte Daten liest, jedes data-*-Attribut, über das die Seite Initialzustand übergibt: extrahiere alles davon.
Lege ein typisiertes API-Modul an:
// api/notifications.ts
export async function fetchNotifications(userId: string) {
const res = await fetch(`/api/v1/notifications?userId=${userId}`);
if (!res.ok) throw new Error('Failed to load notifications');
return res.json() as Promise<Notification[]>;
}
Wenn die servergerenderte Seite Initialdaten über data-*-Attribute oder Inline-<script>-Blöcke übergibt, nimm das für den ersten Sprint als Props der React-Insel an. Plane die Umstellung auf einen echten API-Fetch für den zweiten Sprint. Beides zu mischen ist vorübergehend in Ordnung; entscheidend ist, dass die React-Komponente nie aus dem lebenden DOM außerhalb ihres eigenen Teilbaums liest.
Schritt 4: Insel für Insel ausweiten, nicht Seite für Seite
Der häufigste Fehler nach einer erfolgreichen ersten Insel ist der Wechsel zu einer seitenweisen Migrationsstrategie: "Lass uns die Einstellungsseite komplett umstellen." Dieser Ansatz holt das Rewrite-Risiko auf Seitenebene zurück.
Bleib beim Insel-Muster über mehrere Seiten hinweg. Wenn dein Notification Center läuft, migriere den Date Picker überall dort, wo er vorkommt. Danach die Datei-Upload-Komponente. Jede Insel-Migration ist kleiner als eine Seiten-Migration, geht schneller live und liefert ein testbares Inkrement in die Produktion.
Wenn du sechs bis acht Inseln migriert hast, wirst du feststellen, dass manche Seiten bereits zu 80 Prozent React sind. Das verbleibende jQuery auf diesen Seiten ist typischerweise Navigations-Kleber, den du an einem Nachmittag ersetzen kannst.
Das ist auch die Phase, in der ein Review zur Legacy-Code-Optimierung Muster sichtbar machen kann, die es zu standardisieren lohnt: Event-Bus-Konventionen, geteilter Context für den Authentifizierungszustand, Platzierung von Error Boundaries. Diese Grundlagen richtig zu setzen, bevor die Migration skaliert, spart erhebliche Nacharbeit.
Schritt 5: Geteilten Zustand ohne globalen Store handhaben
Die schwierigste Frage beim Übergang von jQuery zu React ist nicht das UI-Rendering, sondern Zustand, den mehrere Inseln teilen müssen. Ein Nutzer ändert eine Einstellung in einer Insel, und eine andere Insel muss diese Änderung widerspiegeln.
Die jQuery-Antwort war meist eine globale Variable oder ein Custom Event auf document. Beides funktioniert mit Reacts Komponentenmodell nicht sauber.
Stütze dich in den frühen Phasen auf den Server als Single Source of Truth. Wenn Insel A Daten aktualisiert, löst sie eine Mutation aus, und Insel B lädt in einem kurzen Polling-Intervall oder nach einer Nutzeraktion neu. Das ist langsamer als ein Echtzeit-Event, erfordert aber keinerlei Architektur für geteilten Zustand.
Wenn Polling nicht mehr reicht, führe einen leichtgewichtigen Event-Bus über das native CustomEvent des Browsers ein:
// shared/eventBus.ts
export function publish(event: string, detail: unknown) {
window.dispatchEvent(new CustomEvent(`wt:${event}`, { detail }));
}
export function subscribe(event: string, handler: (detail: unknown) => void) {
const listener = (e: Event) => handler((e as CustomEvent).detail);
window.addEventListener(`wt:${event}`, listener);
return () => window.removeEventListener(`wt:${event}`, listener);
}
React-Komponenten räumen ihre Subscriptions in den Return-Callbacks von useEffect auf. jQuery-Code ruft publish auf. Die beiden Schichten bleiben entkoppelt.
Führe eine echte State-Management-Bibliothek (Zustand, Redux Toolkit) erst ein, wenn das Event-Bus-Muster unhandlich wird. Diese Schwelle liegt höher, als die meisten Teams erwarten.
Schritt 6: jQuery Modul für Modul ausmustern
Sobald Inseln die relevante UI-Oberfläche einer Seite abdecken, ist das verbleibende jQuery meist:
- Navigations-Event-Handler (Abfangen von Link-Klicks, Umgang mit dem Zurück-Button-Zustand)
- Seiteninitialisierungscode, der Daten aus dem DOM sammelt und APIs aufruft
- Alte Animations- oder Tooltip-Bibliotheken, die nie ersetzt wurden
Navigations-Handler solltest du zuletzt migrieren, nicht zuerst. Sie sind risikoreich und oft eng an servergerendertes Routing gekoppelt. Lass sie laufen, bis du eine ordentliche Routing-Schicht hast.
Seiteninitialisierungscode, der das DOM ausliest, um servergesendete Daten zu extrahieren, sollte in der API-Migrationsphase (Schritt 3) ersetzt werden. Wenn du hier ankommst, sollte das meiste davon bereits verschwunden sein.
Alte UI-Bibliotheken (jQuery UI, Select2, Chosen) lassen sich oft über useRef und die imperative API der Bibliothek in eine React-Komponente wrappen. Der React-Wrapper verwaltet den Lebenszyklus, die Legacy-Bibliothek übernimmt das Rendering. Das ist nicht ideal, erlaubt dir aber, den Austausch schwergewichtiger UI-Komponenten zu verschieben, bis die umgebende Struktur stabil ist.
Die Migration muss nicht fertig werden
Eine kontraintuitive Erkenntnis von Teams, die inkrementelle Migrationen gut durchziehen: Die Migration muss keine 100 Prozent erreichen. Eine Codebasis, die zu 85 Prozent React ist und zu 15 Prozent jQuery auf Seiten mit wenig Traffic, hat den Großteil der Wartbarkeits- und Geschwindigkeitsvorteile bereits eingesammelt. Die Kosten, diese letzten 15 Prozent zu eliminieren, übersteigen ihren Nutzen oft.
Setze eine Schwelle: "Kein neues jQuery ist erlaubt; bestehendes jQuery nur auf Seiten unterhalb einer bestimmten Traffic-Schwelle." Und betrachte das als erledigt. Das Ziel war immer, die Blutung zu stoppen und das Team voranzubringen, nicht Reinheit zu erreichen.
Hilfe bei den schwierigen Teilen
Die hier beschriebenen Bausteine (Build-Tooling, Insel-Architektur, API-Extraktion, Muster für geteilten Zustand) sind unkompliziert, wenn die Codebasis gut strukturiert ist. Wenn sie das nicht ist, und die meisten jQuery-Anwendungen sind es nicht, bringt jeder Schritt Überraschungen zutage: eng gekoppelte DOM-Abhängigkeiten, fehlende API-Abdeckung, undokumentierte Geschäftslogik in Event-Handlern.
Wenn dein Team kurz vor dieser Migration steht und du eine Außensicht darauf willst, wo das Risiko konzentriert ist, ist das genau die Art von Arbeit im Bereich Webanwendungsentwicklung und Legacy-Code-Optimierung, die wir bei Wolf-Tech machen.
Schreib uns an hello@wolf-tech.io oder besuche wolf-tech.io und schildere deine Situation. Schon ein kurzes Gespräch über die Architektur vor dem ersten Sprint kann die Fehler verhindern, die einer Migration Monate hinzufügen.
FAQ
Funktioniert der inkrementelle Ansatz auch, wenn die jQuery-Anwendung überhaupt keine Build-Pipeline hat?
Ja. Vite neben einer Script-Tag-jQuery-Anwendung hinzuzufügen ist ein eigenständiger Schritt, der wenige Stunden dauert. Die Legacy-Skripte bleiben unangetastet.
Sollten wir jQuery-Tests gleichzeitig auf React Testing Library umstellen?
Nein. Migriere zuerst die UI und rüste die Tests nach, sobald die React-Komponente stabil ist. Beides gleichzeitig zu versuchen verlangsamt beides.
Was, wenn wir parallel auch von einem alten Backend-Framework aktualisieren?
Trenne die Migrationen. Eine Frontend- und eine Backend-Migration, die gleichzeitig laufen, multiplizieren das Risiko der jeweils anderen. Zieh die Frontend-Migration zuerst durch und lass das Backend nachziehen, oder umgekehrt, aber nicht beides auf einmal, es sei denn, sie sind wirklich unabhängig.
Wann funktioniert der inkrementelle Ansatz nicht?
Wenn das jQuery-Frontend so tief an die servergerenderte HTML-Struktur gekoppelt ist, dass jede Komponente von DOM-Zustand abhängt, den jede andere Komponente setzt. In dem Fall muss das Kopplungsproblem vor der Migration gelöst werden, und die Lösung kann zuerst ein begrenztes serverseitiges Refactoring erfordern.

