← Back to Blog
nextjsreactapp-routerarchitecture

New Year, New Router: What I Actually Learned Moving to Next.js App Router

January is the best time to upgrade your mental model. The App Router isn't just a new file structure — it changes how you think about server vs. client, data fetching, and caching. Here's what finally clicked.

KB

When Next.js shipped the App Router I read the docs, followed a tutorial, and thought I understood it. Then I started building something real and realized I'd missed most of the mental model shift.

Here's what actually changed.

The default is server, not client

In the Pages Router, every component was a client component by default. You opted into server behavior with getServerSideProps or getStaticProps.

In the App Router, every component is a server component by default. You opt into client behavior with "use client" at the top of the file.

This is backwards from what most React developers are used to. The practical implication: most of your UI can fetch data directly, without any state management or useEffect, because it runs on the server.

// This runs on the server. No useState, no useEffect, no loading state.
export default async function BlogPage() {
  const posts = await getPosts(); // direct DB call or fetch
  return <PostList posts={posts} />;
}

Client components are islands, not the default

You only need "use client" when a component uses browser APIs, event handlers, or React hooks. Everything else can stay on the server.

The mistake I made early on was adding "use client" to components that didn't need it — habit from the Pages Router. That moves the component and all its children to the client bundle, which grows your JS bundle and loses the server benefits.

Keep client components as leaves in the tree. The closer to the leaf, the better.

Caching is now explicit

The Pages Router had getStaticProps (static), getServerSideProps (dynamic), and ISR (revalidate on interval). The App Router replaces all of this with a single fetch API where you declare caching behavior per request:

// cached indefinitely (static)
fetch(url, { cache: "force-cache" });

// never cached (dynamic)
fetch(url, { cache: "no-store" });

// revalidate every 60 seconds (ISR equivalent)
fetch(url, { next: { revalidate: 60 } });

This is more granular than the old model — you can have some fetches be static and others dynamic within the same page.

Layouts are real now

Layouts in the App Router are React components that wrap their children and persist across navigation. They don't re-render when you navigate between child routes.

This makes navbars, sidebars, and shell UIs genuinely efficient — they mount once and stay mounted. In the Pages Router you had to fake this with _app.tsx.

The App Router has a steeper initial learning curve than the Pages Router. But once the model clicks, it's significantly more capable. The server-first default alone is worth the switch.