Legacy React zu Next.js App Router: Eine inkrementelle Migration ohne Produktionsausfälle
Die Migration von Create React App (CRA) oder dem Next.js Pages Router zum App Router ist eines dieser Projekte, das am Montag einfach aussieht und am Freitag demütigend ist. Die offizielle Next.js-Dokumentation behandelt den Happy Path. Dieser Artikel behandelt den realen Weg - den mit Legacy-Auth-Middleware, geteiltem Zustand zwischen alten und neuen Routen, Hydration-Fehlern, die nur in Produktion auftreten, und Geschäftsstakeholdern, die keine Ausfallzeiten tolerieren.
Nach dieser Migrations-Pattern auf mehreren Codebases - das funktioniert tatsächlich.
Warum inkrementelle Migration die einzige praktikable Option ist
Ein "Big Bang"-Rewrite - Feature-Arbeit stoppen, alles auf einmal migrieren, deployen - gelingt für Codebases von bedeutender Größe fast nie. Die Gründe sind vorhersehbar: Edge Cases multiplizieren sich schneller, als du sie lösen kannst, die Regressions-Abdeckung ist nie so gut wie du denkst, und der Geschäftsdruck, Features zu liefern, pausiert nicht für deinen Migrations-Zeitplan.
Der inkrementelle Ansatz bedeutet, dein altes Routing neben dem neuen App Router laufen zu lassen, Route für Route zu migrieren und dabei kontinuierlich zu deployen. Du erhältst sofort App Router Vorteile in neuen und kürzlich berührten Seiten, während die Legacy-Routen unberührt weiter funktionieren.
So empfiehlt Next.js selbst die Migration vom Pages Router. Für Teams, die von CRA kommen, gibt es einen zusätzlichen Schritt: Du musst die Applikation zuerst in einen Next.js-Host bringen.
Schritt 1: CRA-Nutzer - erst in Next.js einsteigen
Wenn du von Create React App (oder Vite mit einem React SPA-Setup) startest, ist deine erste Aufgabe nicht der App Router. Es ist, deine Applikation überhaupt erst in Next.js zum Laufen zu bringen, als Single-Page-App, bevor du irgendein Routing migrierst.
Erstelle ein minimales Next.js-Projekt neben deinem vorhandenen Code oder ersetze die CRA-Build-Konfiguration, während du alle deine Komponenten unberührt lässt. Das Ziel dieses Schritts ist ein Next.js-app/-Verzeichnis mit einer einzigen Catch-All-Route, die deine vorhandene CRA-App rendert:
// app/[[...slug]]/page.tsx
'use client'
import App from '../../src/App' // deine vorhandene CRA-Root
export default function LegacyApp() {
return <App />
}
Das klingt zu einfach, um wahr zu sein, aber es funktioniert. Deine React Router Routen werden weiterhin clientseitig funktionieren. Du tauschst nur die Build-Infrastruktur aus. Führe deine vollständige Test-Suite gegen diese aus, bevor du irgendetwas anderes anfasst.
Häufige Blocker in dieser Phase:
- Umgebungsvariablen. CRA verwendet
REACT_APP_*; Next.js verwendetNEXT_PUBLIC_*für clientzugängliche Variablen. Du musst Referenzen aktualisieren oder einen Kompatibilitäts-Shim innext.config.jserstellen. - Absolute Imports. CRA richtet
src/als Root ein. Next.js verwendettsconfig.json-Pfad-Aliase - füge einenpaths-Eintrag hinzu, der@/*odersrc/*auf den richtigen Ort zeigt. - Öffentliche Assets. CRAs
public/-Ordner wird direkt zugeordnet; Next.js behandelt das ähnlich, aber überprüfe, dass deineindex.html-Meta-Tags und benutzerdefinierten Schriften durch das Next.js<head>-Äquivalent ersetzt werden.
Sobald deine CRA-App unter Next.js läuft - gleiches Verhalten, nur anderer Dev-Server - bist du bereit, mit der eigentlichen Migration zu beginnen.
Schritt 2: Pages Router und App Router parallel betreiben
Next.js 13+ unterstützt das gleichzeitige Betreiben des Pages Routers (pages/) und des App Routers (app/). Das ist das Fundament der inkrementellen Migration.
Die Regel ist einfach: Wenn eine Route in app/ existiert, hat sie Vorrang. Wenn sie in app/ nicht existiert, fällt Next.js auf pages/ zurück. Du kannst eine einzelne Seite migrieren, ohne irgendetwas anderes anzufassen.
Richte deine Projektstruktur so ein:
/app
layout.tsx <- neues Root-Layout
dashboard/
page.tsx <- migrierte Route
/pages
_app.tsx <- Legacy App-Wrapper
_document.tsx <- Legacy-Dokument
settings.tsx <- noch nicht migriert
profile.tsx <- noch nicht migriert
Die app/layout.tsx-Datei ist jetzt deine Root für migrierte Routen. Alles in pages/ funktioniert weiter mit dem alten _app.tsx-Wrapper.
Kritisch: globale CSS-Konflikte vermeiden. Der App Router verwendet layout.tsx-Imports für CSS; der Pages Router verwendet _app.tsx-Imports. Wenn du ein globales Stylesheet in beiden importierst, wird es in manchen Zuständen zweimal geladen. Prüfe deine CSS-Imports sorgfältig und erwäge, gemeinsame Styles in ein Paket oder eine dedizierte Datei zu verschieben, die von beiden Wrappern explizit importiert wird.
Schritt 3: Geteilter Authentifizierungszustand
Auth ist der schwierigste Teil der inkrementellen Migration und der häufigste Grund, warum Teams zurückrollen.
Wenn du eine clientseitige Auth-Bibliothek wie Auth0, Clerk oder einen benutzerdefinierten JWT-in-localStorage-Ansatz verwendest, besteht die Herausforderung darin, dass deine App Router Seiten und deine Pages Router Seiten den Auth-Zustand nahtlos teilen müssen - Nutzer dürfen sich nicht erneut anmelden müssen, wenn sie zwischen einer alten und einer neuen Route navigieren.
Für Token-basierte Auth (JWT in httpOnly Cookie): Das ist der einfachste Fall. Der Cookie ist für alle Routen verfügbar, unabhängig davon, welcher Router die Seite verarbeitet. Deine Middleware oder Server Components können ihn direkt lesen. Migriere zu diesem Muster, bevor du mit der App Router Migration beginnst, wenn du noch keine httpOnly-Cookies verwendest.
Für clientseitige Auth-Provider (Context-basiert): Du benötigst denselben Provider, der sowohl app/layout.tsx als auch pages/_app.tsx umhüllt. Erstelle eine gemeinsame AuthProvider-Komponente, die außerhalb beider Router-Verzeichnisse lebt, und importiere sie von beiden:
// components/providers/AuthProvider.tsx
'use client'
export function AuthProvider({ children }: { children: React.ReactNode }) {
// deine Auth-Logik hier
return <AuthContext.Provider value={...}>{children}</AuthContext.Provider>
}
Dann in app/layout.tsx:
import { AuthProvider } from '@/components/providers/AuthProvider'
export default function RootLayout({ children }) {
return (
<html>
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
)
}
Und in pages/_app.tsx:
import { AuthProvider } from '../components/providers/AuthProvider'
export default function App({ Component, pageProps }) {
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
)
}
Beide Router teilen jetzt dieselbe Auth-Komponente. Der Provider initialisiert sich beim Mount aus dem Cookie oder Token, sodass der Zustand über Navigation hinweg konsistent ist.
Wenn du Hilfe bei der Gestaltung einer Auth-Architektur benötigst, die diese Migration übersteht, bietet Wolf-Tech Architektur-Reviews speziell für diese Art von querschnittlichen Anliegen.
Schritt 4: Route für Route migrieren
Mit dem parallelen Setup auf der Stelle beginne, einzelne Routen zu migrieren. Eine gute Reihenfolge:
- Statische oder nahezu statische Seiten zuerst (Marketing-Seiten, Docs, Rechtliches). Diese haben minimale Zustand- und Datenanforderungen, sodass sie dein Setup mit geringem Risiko validieren.
- Datenabruf-Seiten als nächstes. Das Ersetzen von
getServerSidePropsoderuseEffect-Datenabruf durch Server Components undfetch()ist der sichtbarste Vorteil des App Routers. Nimm es Route für Route. - Komplexe interaktive Seiten zuletzt. Dashboards, formularintensive Flows und Seiten mit erheblichem Client-Zustand sollten zuletzt migriert werden, nachdem du Vertrauen in deinen Ansatz gewonnen hast.
Beim Migrieren einer Route verschiebe oder erstelle die Datei aus pages/ in app/ neu. Entscheide, welche Teile Server Components sind (Standard) und welche 'use client' benötigen. Eine häufige Heuristik: Wenn die Komponente useState, useEffect, Event-Handler oder Browser-APIs verwendet, benötigt sie 'use client'. Alles andere kann eine Server Component sein und wird davon profitieren.
Schritt 5: Hydration-Mismatch-Fallen vermeiden
Hydration-Fehler - bei denen das serverseitig gerenderte HTML nicht mit dem übereinstimmt, was React auf dem Client produziert - sind der häufigste nur-in-Produktion-Fehlermodus bei App Router Migrationen.
Die häufigsten Ursachen:
Browser-only-Code in Server Components. Wenn du window, document oder localStorage in einer Komponente ohne 'use client' referenzierst, erhältst du in Produktion einen Hydration-Fehler (nicht immer in Entwicklung, weshalb es Teams überrascht). Verschiebe diesen Code in ein useEffect oder in eine Client-Komponente.
Datum- und Locale-Rendering. new Date().toLocaleDateString() erzeugt auf dem Server (typischerweise UTC) und dem Client (Locale des Nutzers) unterschiedliche Ausgaben. Verwende explizite Locale-Argumente: date.toLocaleDateString('de-DE', { timeZone: 'UTC' }).
Bedingtes Rendering basierend auf typeof window. Das klassische SPA-Muster:
// Verursacht Hydration-Mismatch
const isClient = typeof window !== 'undefined'
return isClient ? <ClientOnlyThing /> : null
Ersetze dies durch dynamische Imports mit ssr: false:
import dynamic from 'next/dynamic'
const ClientOnlyThing = dynamic(() => import('./ClientOnlyThing'), { ssr: false })
Drittanbieter-Bibliotheken, die eine Browser-Umgebung voraussetzen. Viele ältere React-Bibliotheken - Charting-Bibliotheken, Rich-Text-Editoren, Drag-and-Drop-Kits - setzen beim Import voraus, dass window existiert. Umhülle diese mit dynamic() und ssr: false oder verschiebe sie vollständig in 'use client'-Komponenten mit Lazy Loading.
Schritt 6: Den Pages Router bereinigen
Sobald alle Routen live in app/ sind, werden die Pages Router Dateien zu totem Code. Entferne pages/ inkrementell, sobald Routen in Produktion als stabil bestätigt sind - idealerweise mit einem Feature-Flag oder Canary-Deployment für jede Charge.
Beeil dich bei diesem Schritt nicht. Die Versuchung ist, pages/ sofort zu löschen, sobald die letzte Route migriert ist, aber es lohnt sich, die alten Dateien für ein oder zwei Deployment-Zyklen als Rollback-Sicherheitsnetz zu behalten. Sobald du Vertrauen in die App Router Routen unter echtem Traffic hast, lösche die entsprechenden pages/-Dateien.
Nachdem pages/ leer ist (außer API-Routen, wenn du sie behältst), kannst du die Pages Router Konfiguration aus next.config.js entfernen und pages/_app.tsx sowie pages/_document.tsx löschen.
Was Teams zum Zurückrollen zwingt
Basierend auf Migrationen, die schiefgingen, sind das die zu beobachtenden Fehlermuster:
Das Auth-Audit überspringen. Teams, die den Auth-Zustand-Sharing nicht explizit verifizieren, landen bei Nutzern, die auf alten Routen angemeldet, auf neuen abgemeldet erscheinen, oder umgekehrt. Das ist ein Produktionsvorfall, kein Entwicklungsumgebungs-Bug.
Zu schnell migrieren. Fünf oder zehn Routen in einem einzigen PR macht einen Rollback nahezu unmöglich, wenn etwas schiefgeht. Eine oder zwei Routen pro PR, mit QA dazwischen, ist das richtige Tempo.
Middleware-Kompatibilität ignorieren. Next.js Middleware (middleware.ts) läuft auf allen Routen, verhält sich aber in einigen Edge Cases unterschiedlich zwischen den beiden Routern - insbesondere rund um Redirects und Cookie-Zugriff. Teste deine Middleware explizit gegen beide alten und neuen Routen.
Den Drittanbieter-Bibliotheksaufwand unterschätzen. Einige Komponenten-Bibliotheken haben App Router-spezifische Versionen oder Konfigurationen. React Query beispielsweise erfordert ein spezifisches Provider-Setup für den App Router. Überprüfe die App Router Dokumentation jeder großen Abhängigkeit, bevor du Seiten migrierst, die davon abhängen.
Der realistische Zeitplan
Ein realistischer Migrations-Zeitplan für ein mittelgroßes SaaS (30-60 Routen, ein Entwicklungsteam):
- Woche 1-2: CRA zu Next.js Host-Setup (falls anwendbar), Pages Router Parallel-Setup, Auth-Provider-Extraktion, Entwicklungsumgebung validiert
- Woche 3-6: Route-für-Route-Migration, statische und datenabrufende Seiten, 2-3 Routen pro Woche
- Woche 7-10: Komplexe interaktive Routen, Drittanbieter-Bibliotheks-Migrationen, Regressionstest
- Woche 11-12: Pages Router Bereinigung, finale QA, Dokumentation
Das ist ein vier-Sprint-Projekt, kein einzelner Sprint. Es als irgendetwas Kürzeres zu planen ist der Ort, wo Zeitpläne kollabieren.
Lohnt sich das?
Die App Router Vorteile sind real: serverseitiges Rendering ohne das getServerSideProps-Boilerplate, paralleles Datenabrufen mit Suspense, verschachtelte Layouts ohne Prop-Drilling und deutlich bessere Performance für datenintensive Seiten. Für Teams, die inhaltsreiche oder datenreiche Applikationen bauen, sind die Verbesserungen messbar.
Für Teams mit einer stabilen, funktionierenden Pages Router Codebase ohne unmittelbare Schmerzpunkte lohnt sich die Migration zum Planen, aber nicht zum Überstürzen. Der hier beschriebene inkrementelle Weg bedeutet, dass du sofort Vorteile auf neuen Routen ernten kannst, während stabile Routen unberührt bleiben.
Wenn du mit einer CRA-Codebase umgehst, die zu einem Wartungsproblem geworden ist - langsame Builds, kein SSR, Abhängigkeitskonflikte - zahlt sich die Migration schneller aus und die Dringlichkeit ist höher.
Hilfe holen
Legacy-zu-Modern-Migrationen sind ein bedeutender Teil dessen, was Wolf-Tech macht, und der React/Next.js-Weg speziell ist einer, den wir gut kennen. Wenn dein Team diese Migration plant und einen Experten möchte, der deinen Ansatz überprüft, bevor du beginnst - oder um eine ins Stocken geratene Migration wieder in Gang zu bringen - melde dich unter hello@wolf-tech.io oder besuche wolf-tech.io für eine kostenlose Erstberatung.
Das Ziel ist es, dich zum App Router zu bringen, ohne einen Produktionsvorfall. Mit dem richtigen inkrementellen Plan ist das vollständig erreichbar.

