Von Legacy React zum Next.js App Router: Eine inkrementelle Migration ohne Produktionsunterbrechung
Die Migration von React zu Next.js gehoert zu den meist gefragten -- und meist missverstandenen -- Modernisierungsschritten in der aktuellen SaaS-Landschaft. Teams sehen die Server Components des App Routers, das eingebaute Caching und das Layout-Nesting und wollen das haben. Was sie unterschaetzen, ist die Distanz zwischen dem Wollen und einer Produktionscodebase, die das tatsaechlich nutzt.
Dieser Leitfaden richtet sich an Teams, die ein echtes Create React App (CRA)- oder Pages-Router-Projekt in Produktion betreiben. Kein Greenfield. Kein Demo. Eine Codebase mit echten Nutzern, einer Authentifizierungsschicht, gemeinsamem State und einer Deploy-Pipeline, die nicht drei Sprints lang dunkel bleiben kann, waehrend jemand alles von Grund auf neu baut.
Die gute Nachricht: ein Rewrite ist nicht noetig. Die schlechte Nachricht: eine inkrementelle Migration erfordert eine Disziplin, die "einfach alles neu schreiben" nicht braucht.
Warum inkrementell der einzig realistische Weg ist
Vollstaendige Rewrites wirken auf dem Papier sauber. In der Praxis erzeugen sie ein Moving-Target-Problem: Wenn der Rewrite fertig ist, hat die urspruengliche Codebase sechs Wochen Produktaenderungen eingebracht, die vorwaerts portiert werden muessen. Teams betreiben dann zwei Codebases parallel, verbrennen Engineering-Zeit mit Synchronisation statt mit Produktarbeit, und am Ende wird der Rewrite entweder halbfertig ausgeliefert oder aufgegeben.
Die inkrementelle Migration laesst die Original-App auf jeder Route, die noch nicht angefasst wurde, weiterhin Traffic bedienen. Eine Route oder einen Feature-Cluster nach dem anderen migrieren, in Produktion validieren, dann weitermachen. Das Ergebnis ist eine langsamere Headline-Zahl, aber ein wesentlich geringeres Risiko des katastrophalen Mid-Migration-Rollbacks.
Der Haken: "inkrementell" erfordert eine echte Naht zwischen Alt und Neu. Und genau diese Naht uebergehen die meisten Teams bei der Planung.
Schritt eins: Die aktuelle Architektur auditieren, bevor eine Zeile Code geschrieben wird
Bevor Next.js angefasst wird, einen Tag damit verbringen, diese Fragen zur bestehenden App zu beantworten:
Routing. Wie viele Top-Level-Routen gibt es? Sind davon welche dynamisch generiert? Haelt der Router State (Query-Params, History-Manipulation)?
Authentifizierung. Wo lebt die Auth? Nur clientseitig (localStorage, in-memory)? Serverseitige Sessions? Ein Drittanbieter wie Auth0 oder Clerk? Die Antwort praegt alles daran, wie Auth-State zwischen Alt und Neu geteilt wird.
Datenabruf. Wird React Query, SWR, Redux Thunk oder reines useEffect verwendet? Diese Muster haben unterschiedliche Entsprechungen in den Server Components des App Routers.
Geteilte UI. Gibt es eine Komponentenbibliothek oder ein Design-System? Koennen diese Komponenten in einem Server-Kontext laufen, oder setzen sie auf window, document oder Browser-APIs?
Dieses Audit ist keine Fleissarbeit. Es produziert eine Migrations-Map, die zeigt, welche Routen zuerst sicher zu verschieben sind (schwache Kopplung, einfacher Datenabruf) und welche echte Architekturarbeit benoetigen (komplexe Auth-Flows, starker Client-State).
Schritt zwei: Die Adapter-Schicht -- beide Router gleichzeitig betreiben
Das zuverlaessigste Muster fuer die Migration einer CRA- oder Pages-Router-App zum App Router ist, beide nebeneinander zu betreiben -- mittels Next.js' eigenem Inkremental-Adoptions-Support.
Wer von CRA startet, besteht der erste Schritt darin, die bestehende App ueberhaupt in einer Next.js-Shell zum Laufen zu bringen -- ohne jegliche Anwendungslogik zu aendern. Das bedeutet typischerweise:
- Ein Next.js-Projekt neben (oder als Wrapper um) die CRA-App aufsetzen
- Eine Catch-All-Route (
[[...slug]].tsxim Pages Router, oder ein Layout + Catch-All im App Router) verwenden, um alle nicht erkannten Pfade an die Legacy-SPA-Shell weiterzuleiten - Routen schrittweise herausloesen und als echte Next.js-Seiten neu aufbauen
Wer bereits auf dem Pages Router ist, hat einen einfacheren Ausgangspunkt. Next.js unterstuetzt das gleichzeitige Betreiben von Pages-Router- und App-Router-Seiten im selben Projekt. Routen unter app/ verwenden den App Router; Routen unter pages/ verwenden weiterhin den Pages Router. Das ist offiziell unterstuetzt und wird von grossen Teams bei Vercel und anderswo in Produktion eingesetzt.
Die entscheidende Disziplin: dem Drang widerstehen, alles auf einmal zu migrieren, nur weil die Tuer offen ist.
Schritt drei: Gemeinsamer Authentifizierungs-State
Authentifizierung ist der Punkt, an dem inkrementelle Migrationen am haeufigsten scheitern. Das Fehlerbild: Die neuen App-Router-Seiten koennen die Session der alten Auth-Schicht nicht lesen, und der Nutzer landet auf einer eingeloggten Seite und wird zum Login-Screen weitergeleitet.
Die Loesung haengt vom Auth-Provider ab:
Cookie-basierte Sessions (NextAuth, eigene Server-Sessions). Diese funktionieren natuerlich ueber beide Router hinweg, weil das Session-Cookie serverseitig gelesen wird. NextAuth v5 (Auth.js) hat explizite Unterstuetzung fuer den App Router, und sein auth()-Helper funktioniert sowohl in Server Components als auch in Route Handlern.
Clientseitige Auth (Auth0 SDK, Clerk, eigenes JWT in localStorage). Ein Provider-Wrapper wird benoetigt. Die App-Router-Layouts im entsprechenden Context-Provider wrappen (Clerks <ClerkProvider>, Auth0s <Auth0Provider>), genau wie in einer CRA-App. Die Komplikation: Server Components koennen keinen Client-Context lesen -- daher muss serverseitig abgerufene Daten, die die User-Identitaet benoetigen, aus dem Cookie oder der Session gelesen werden, nicht aus dem Client-State.
Custom in-memory Auth. Das ist der schwerste Fall. Die Auth lebt nirgendwo dauerhaft. Es wird noetig sein, die Auth zu einem Cookie- oder Server-Session-Modell zu migrieren, bevor die App-Router-Migration Sinn ergibt. Das zuerst als separaten Arbeitsschritt erledigen.
Die Faustregel: Wenn der Auth-State dauerhaft ist (Cookie, Server-Session, JWT in einem Cookie), ist die Migration einfach. Wenn er nur im Client-Memory oder localStorage lebt, diesen Grenzwert zuerst beheben.
Schritt vier: Route fuer Route, beginnend am risikoarmen Ende
Mit aufgeloester Adapter-Schicht und geklaetem Auth-Grenzwert koennen Routen migriert werden. Eine gute Reihenfolge:
-
Zuerst statische oder nahezu statische Seiten. About-Seiten, Preisseiten, Marketing-Landing-Pages. Wenig State, keine Auth-Abhaengigkeit, und die statische Generierung des App Routers macht sie unkompliziert schneller.
-
Danach authentifizierte Datenanzeige-Seiten. Dashboard-Ansichten, die Daten abrufen und anzeigen, aber keine komplexen Mutation-Flows haben. Diese sind der beste Showcase fuer Server Components -- Daten serverseitig abrufen, an den Client streamen und das Lade-Spinner-bei-jeder-Navigation-Muster von SWR/React Query vermeiden.
-
Formulare und Mutations-Flows zuletzt. Jede Seite mit einem Formular, das die API auf komplexe Weise beruehrt, sollte zuletzt kommen. Server Actions sind leistungsstark, haben aber andere Fehlerbehandlungs-Semantik als ein
useMutation-Hook, und man moechte sicher im Muster sein, bevor man es auf kritische User Journeys anwendet.
Fuer jede Route ist die Migrations-Checkliste:
- Datei von
pages/nachapp/mit einem Layout verschieben -
getServerSidePropsodergetStaticPropsdurch asynchronen Server-Component-Datenabruf ersetzen - Jede Komponente identifizieren, die
useState,useEffectoder Browser-APIs nutzt --'use client'-Direktive hinzufuegen - Auth korrekt aus der Server-Session lesen lassen
- Route durch die E2E-Test-Suite laufen lassen (oder eine schreiben, bevor migriert wird, wenn keine existiert)
Hydrations-Mismatches: Der stille Migrations-Killer
Der haeufigste Grund, warum Teams eine migrierte Route zurueckrollen, ist ein Hydrations-Mismatch-Fehler. Das passiert, wenn das serverseitig gerenderte HTML nicht dem entspricht, was React clientseitig waehrend der Hydration produziert.
Haeufige Ursachen waehrend der Migration:
Datum- und Zeitrendering. Wenn ein Timestamp gerendert und mit der Locale des Nutzers clientseitig formatiert wird, produziert der Server einen anderen String als der Browser. Ein stabiles Format (ISO oder UTC) fuer SSR verwenden und Locale-Formatierung erst nach dem Mount anwenden.
Browser-only-Bibliotheken. Jede Bibliothek, die intern typeof window !== 'undefined' prueft, signalisiert damit, dass sie nicht sicher in einem Server-Kontext ausgefuehrt werden kann. Diese in einen dynamischen Import mit ssr: false einwickeln.
Bedingtes Rendering basierend auf Auth-State. Wenn unterschiedliche UI angezeigt wird, je nachdem ob der Nutzer eingeloggt ist, und das in der alten App aus dem Client-State abgeleitet wird, in der neuen aber aus einer Server-Session, entsteht ein Mismatch. Fuer jede migrierte Route auf eine einzige Quelle der Wahrheit fuer die Auth festlegen, bevor sie live geht.
Hydrations-Fehler sind nicht katastrophal -- React faellt auf clientseitiges Rendering zurueck -- aber sie erzeugen Konsolen-Laerm, unterbrechen Streaming und signalisieren, dass etwas in den Rendering-Annahmen nicht stimmt. Waehrend der Migration als Blocker behandeln, nicht als spaeter zu bereinigende Annoyances.
Wann wirklich gestoppt und refaktoriert werden muss
Der inkrementelle Pfad setzt voraus, dass der Komponentencode portierbar ist. Manche Codebases haben strukturelle Probleme, die die Migration unabhaengig von der Sorgfalt blockieren:
- Tief gekoppelter Client-State. Wenn der globale Redux-Store die Quelle der Wahrheit fuer Daten ist, die serverseitig leben sollten, wird jede Server-Component-Grenze zum Kampf. Eine State-Architektur-Refaktorierung parallel zur Migration planen.
- Monolithische Page-Komponenten. Wenn Seiten aus 1.500-Zeilen-Komponenten bestehen, in denen alles vermischt ist, ist das Aufteilen in Server-Component-Shells mit Client-Component-Blaettern schmerzhaft. Zuerst die Komponentenstruktur aufbrechen.
- Keine Testabdeckung auf kritischen Pfaden. Eine Route zu migrieren, die nicht verifiziert werden kann, ist eine Risikoverlagerung, keine Modernisierung. Integrations- oder E2E-Tests fuer jede Route schreiben, bevor sie verschoben wird.
Wolf-Techs Legacy-Code-Optimierungs-Service beginnt haeufig mit genau dieser Art von Struktur-Audit -- zu identifizieren, welche Teile einer Codebase so wie sie sind verschiebbar sind und welche Vorarbeit benoetigen, bevor eine Migration sinnvoll ist. Dasselbe Denken gilt hier.
Die Post-Migrations-Bereinigung, ueber die man froh sein wird
Wenn alle Routen auf dem App Router sind, das Projekt noch nicht schliessen. Die Adapter-Schicht und Catch-All-Routen, die waehrend der Migration hinzugefuegt wurden, sollten entfernt werden. Tote pages/-Dateien geloescht werden. Die _app.tsx- und _document.tsx-Wrapper aus dem Pages Router haben in einem reinen App-Router-Projekt keine Funktion mehr.
Noch wichtiger: Jetzt ist der Moment, die Datenabruf-Strategie mit App-Router-Semantik zu ueberdenken. Muster, die unter dem Pages Router Sinn ergaben (aggressives clientseitiges Caching mit React Query, weil getServerSideProps teuer war), sehen anders aus, wenn Server Components nah an der Datenquelle abrufen koennen. Nicht alles muss sich aendern, aber das mentale Modell "serverseitig abrufen, an den Client streamen, nur das hydrieren, was Interaktivitaet benoetigt" ist es wert, konsequent anzuwenden.
Es richtig machen, ohne stecken zu bleiben
Teams, die mitten in der Migration stocken, teilen ein Muster: Sie haben ohne eine klare Naht zwischen Alt und Neu begonnen, bei Route fuenf ein Auth- oder Hydrations-Problem getroffen und hatten nicht das strukturelle Wissen, es schnell zu diagnostizieren. Der Migrations-Branch blieb drei Monate offen, haeufte Konflikte an und wurde schlussendlich aufgegeben oder in einem gebrochenen Zustand gemergt.
Wer eine React-zu-Next.js-Migration plant und vor dem Start eine zweite Meinung zur Architektur moechte -- oder wer bereits mitten in der Migration feststeckt -- arbeitet Wolf-Tech genau in solchen Situationen. Unser Web-Application-Development- und Code-Quality-Consulting-Service beruehren diesen Bereich regelmaessig.
Die Migration lohnt sich. Der App Router verbessert die Performance wirklich, vereinfacht den Datenabruf und stellt das Frontend dahin auf, wohin React sich entwickelt. Der Weg erfordert nur mehr Planung, als die Framework-Dokumentation vermuten laesst.
Unter hello@wolf-tech.io kann die spezifische Situation besprochen werden, bevor man sich fuer einen Ansatz entscheidet.

