Next.js has been the go-to React framework for years, but the App Router is not an incremental upgrade. It is a rethinking of how React applications are structured, rendered, and delivered. Server Components are the default, layouts persist across navigations, and data fetching happens where it should have always happened: on the server, inside the components that need it.
Layouts That Actually Persist
The Pages Router forced a full re-render on every navigation. The App Router introduces nested layouts that survive route changes. A sidebar, a navigation bar, or a music player stays mounted while only the page content swaps.
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
);
}
Every route under /dashboard shares this layout. The Sidebar component does not unmount, does not lose state, and does not re-fetch its data when navigating between dashboard pages. This is not a clever hack. It is the routing primitive.
Data Fetching Without the Ceremony
In the App Router, Server Components fetch data directly. No getServerSideProps, no getStaticProps, no loader functions in a separate export. The component is the data layer.
import { notFound } from 'next/navigation';
async function BlogPost({ params }: { params: { slug: string } }) {
const post = await db.post.findUnique({ where: { slug: params.slug } });
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<time>{new Date(post.date).toLocaleDateString()}</time>
<div>{post.content}</div>
</article>
);
}
export default BlogPost;
The async keyword on a component still feels strange, but the simplicity is undeniable. No state management for loading, no error boundaries wrapping fetch calls, no duplication between what the server knows and what the client needs to re-discover. The data is there when the component renders, period.
Server Actions: The End of API Routes
Server actions let you call server-side functions directly from client components. Form submissions, database mutations, revalidation, all happen in a single function without wiring up an API endpoint.
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function publishPost(postId: string) {
await db.post.update({
where: { id: postId },
data: { status: 'published' },
});
revalidatePath('/posts');
redirect('/posts');
}
'use client';
import { publishPost } from '@/app/posts/actions';
function PublishButton({ postId }: { postId: string }) {
return <button onClick={() => publishPost(postId)}>Publish</button>;
}
The action runs on the server regardless of where it is called. The client component imports a reference, not the implementation. This pattern removes an entire layer of infrastructure that most applications do not need.
Streaming and Suspense: Progressive Rendering
The App Router streams HTML to the browser as it becomes available. Slow data sources do not block the entire page. Wrap a component in Suspense and the rest of the page renders immediately while the slow part loads in.
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<QuickStats />
<Suspense fallback={<TableSkeleton />}>
<RevenueTable />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
</div>
);
}
QuickStats renders instantly. RevenueTable and AnalyticsChart stream in as their data resolves. The user sees a useful page immediately instead of staring at a full-screen spinner. This is not optimistic UI. It is actual progressive rendering at the HTML level.
Parallel and Intercepting Routes
The App Router introduces routing patterns that were previously impossible without client-side hacks. Parallel routes render multiple pages simultaneously in the same layout. Intercepting routes let you show a modal overlay on navigation while preserving the full page as a shareable URL.
import { Modal } from '@/components/modal';
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<Modal>
<Photo id={params.id} />
</Modal>
);
}
Clicking a photo in a feed opens a modal. Refreshing the page loads the full photo page. Sharing the URL works. The routing system handles the complexity that developers used to manage manually with state, portals, and URL synchronization.
The Complexity Tradeoff
The App Router is more powerful than the Pages Router, and it is also more complex. The mental model of Server Components, the "use client" boundary, caching semantics, and the distinction between static and dynamic rendering require a real investment in understanding. But that complexity exists because the problems are complex. The Pages Router hid them behind simpler abstractions that broke down at scale. The App Router surfaces them and gives you the tools to solve them properly.
Next.js with the App Router is not the easiest way to build a React app. It is the most complete. For teams building production applications that need performance, SEO, and maintainability, that completeness is what matters.