Understanding the Shift
The Next.js App Router, introduced as stable in Next.js 13.4 and refined through subsequent releases in 2023 and into 2024, represents the most significant architectural change in the framework's history. Built on React Server Components, it fundamentally changes how data fetching, layouts, rendering, and caching work in Next.js applications.
For teams with existing Next.js applications using the Pages Router, the question of migration is increasingly pressing. The App Router is where the framework's future development effort is concentrated, and new features and optimisations are being built primarily for the App Router architecture. The question is not whether to consider migration but how to approach it without disrupting existing functionality or overwhelming the team.
What Changes with the App Router
The App Router introduces several concepts that represent a meaningful departure from the mental model of the Pages Router. Understanding these differences thoroughly before beginning migration is essential.
React Server Components
Components in the App Router are server-rendered by default — they execute on the server and send only their rendered output to the browser. Client components, which execute in the browser and can use hooks, event handlers, and browser APIs, must be explicitly opted into using the `"use client"` directive at the top of the file.
This is a fundamental inversion of the previous model, where all components were client components by default and server-side rendering was a framework-level concern. The server component model reduces the amount of JavaScript sent to the browser, enables data fetching directly within components, and provides access to server-side resources without exposing them to the client.
Nested Layouts
The App Router supports nested layouts that persist across navigation. A layout defined at a particular route segment wraps all pages and sub-layouts beneath it, and crucially, it does not re-render when the user navigates between pages within that segment. This enables sophisticated UI patterns such as persistent navigation, sidebars that maintain their scroll position, and shared loading states, all without unnecessary re-rendering.
New Data Fetching Patterns
The familiar `getServerSideProps`, `getStaticProps`, and `getInitialProps` functions are replaced by a fundamentally different approach. In the App Router, server components can use async/await syntax directly to fetch data, and the framework provides a fetch API with built-in caching and revalidation options that give fine-grained control over data freshness.
This new model is more flexible and more powerful, but it requires a different way of thinking about where and when data is fetched.
Loading and Error States
Dedicated `loading.tsx` and `error.tsx` files provide automatic handling of loading and error states at the route segment level. When a server component is fetching data, the framework automatically renders the nearest loading file as a fallback. When an error occurs, it is caught by the nearest error boundary. This convention-based approach reduces boilerplate and ensures consistent handling of these states throughout the application.
Streaming and Suspense
The App Router leverages React Suspense boundaries to enable streaming server-side rendering. Rather than waiting for all data to be fetched before sending any HTML to the browser, the server can stream content progressively, sending ready portions of the page immediately and filling in the rest as data becomes available. This can significantly improve perceived performance, particularly for pages with multiple data-fetching requirements of varying speed.
Planning Your Migration
A successful migration requires careful planning and a realistic assessment of the effort involved. The good news is that Next.js supports running the Pages Router and App Router simultaneously within the same application, which enables an incremental approach that avoids the risk of a large-scale rewrite.
Audit Your Existing Application
Before writing any code, catalogue your existing routes, data fetching patterns, shared layouts, middleware usage, and third-party dependencies. Identify which parts of the application would benefit most from migration and which might present complications. Pay particular attention to:
- Pages that use `getServerSideProps` or `getStaticProps` — these will need their data fetching patterns rewritten
- Components that rely heavily on client-side state and effects — understanding the client component boundary is crucial
- Third-party libraries used in your components — not all are compatible with React Server Components
- Custom document and app configurations — the App Router handles these differently
- API routes — these are migrated separately to the new Route Handlers convention
Start with New Features
Rather than migrating existing routes immediately, consider building new features in the App Router. This allows your team to gain familiarity with the new patterns, conventions, and mental model before tackling the complexity of migrating existing functionality. The learning curve for server components and the new data fetching model is not trivial, and it is better to encounter it in the context of new work than whilst simultaneously trying to preserve existing behaviour.
Migrate Route by Route
Move existing routes incrementally, starting with simpler pages that have fewer dependencies and less complex data fetching. Test thoroughly after each migration to ensure no regressions have been introduced. A phased approach might look like:
- Migrate simple static pages with no data fetching
- Move pages with straightforward server-side data fetching
- Tackle pages with complex client-side interactivity
- Address pages with significant third-party library dependencies
- Migrate shared layouts and navigation last, once the patterns are well-established
Common Challenges and Solutions
Several challenges commonly arise during migration, and being prepared for them can save considerable time and frustration.
Client Component Boundaries
Understanding which components need the `"use client"` directive takes practice. Any component that uses React hooks such as useState or useEffect, accesses browser APIs like window or document, or attaches event handlers must be a client component. The key principle is to push client boundaries as far down the component tree as possible, keeping the majority of the tree as server components to maximise the performance benefits.
A common mistake is placing the `"use client"` directive too high in the component tree, which forces everything beneath it to be a client component and negates much of the benefit. Instead, extract the interactive portions into small, focused client components and compose them within server components.
Third-Party Library Compatibility
Not all libraries have been updated for React Server Component compatibility. Libraries that access browser APIs, use React context, or rely on hooks at the module level cannot be used directly in server components. Before migrating a route, check compatibility for all dependencies it uses. Wrapper patterns — where an incompatible library is used within a client component that is composed into a server component — provide a practical solution in many cases.
Data Fetching Refactoring
Migrating from the Pages Router data fetching functions to the App Router patterns requires rethinking how and where data is fetched. The new model allows data fetching to be colocated with the component that needs the data, which often means distributing what was a single data fetching function across multiple components. Understanding the caching and revalidation mechanisms is essential to avoid either over-fetching (making unnecessary requests) or serving stale data.
Metadata and SEO
The App Router introduces a new metadata API that replaces the use of the Head component from the Pages Router. Static and dynamic metadata is defined through exported metadata objects or generateMetadata functions, which provide a more structured approach to managing page titles, descriptions, and Open Graph tags.
Testing and Validation
Thorough testing is essential throughout the migration process. Ensure your test suite covers critical user flows, and consider adding integration tests that verify the interaction between server and client components. Performance monitoring should be in place to confirm that the migration is delivering the expected improvements in bundle size, time to first byte, and overall page performance.
Our Recommendations
At GRDJ Technology, we have guided several client projects through this migration and have developed a structured approach based on that experience. Our consistent advice is to take an incremental approach, invest time upfront in understanding the new mental model, and resist the urge to migrate everything at once. The App Router offers genuine and significant benefits in performance, developer experience, and architectural clarity, but realising those benefits requires a thoughtful, well-planned approach rather than a rushed rewrite.