The Next.js App Router is fundamentally a React Server Component (RSC) engine with a client-side router bolted on.
Let’s see it in action. Imagine a simple app/page.js file:
// app/page.js
async function getData() {
const res = await fetch('https://api.example.com/data', { cache: 'no-store' });
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
const data = await getData();
return (
<div>
<h1>Hello, App Router!</h1>
<p>Data: {data.message}</p>
</div>
);
}
When a user navigates to /, Next.js doesn’t just render this component on the client. Instead, the server processes this Page component as a React Server Component.
Here’s the breakdown of what happens:
-
Server-Side Rendering (SSR) of RSCs: The
Pagecomponent, beingasyncand fetching data, is executed on the server.getData()runs, fetches fromapi.example.com, and returns JSON. The server then renders this component into a special JSON format that describes the React tree and its static content. This isn’t HTML; it’s a serialized representation of the React elements, including the fetched data. -
Bundling and Streaming: Next.js bundles your application code into JavaScript chunks. For RSCs, it sends down the minimal JavaScript needed for hydration on the client and the serialized RSC output. Critically, it can stream this output. If
getData()takes time, the<h1>Hello, App Router!</h1>part might be sent to the browser before the data is ready, allowing the user to see something immediately. The data part is then streamed in as it becomes available. -
Client-Side Router and Hydration: On the client, the Next.js router (built on top of React Router) takes over. It receives the serialized RSC output. The browser’s JavaScript then uses this data to "hydrate" the static HTML that was also sent down (or is being streamed). Hydration is the process of attaching event listeners and making the server-rendered UI interactive on the client. The client-side router manages navigation. When you click a link, it intercepts the navigation, fetches the new RSC data for the target route from the server (again, as serialized RSC output), and updates the DOM without a full page reload.
-
Client Components: If you have Client Components (marked with
'use client'), they are treated differently. They are bundled for the client. When the RSC tree is rendered on the server, it can include placeholders for these Client Components. The server sends down the RSC data, and when the client-side router renders the tree, it knows to load and render the associated Client Component JavaScript. The Client Component then hydrates on the client. -
Data Fetching Strategies:
fetchcalls within RSCs are automatically memoized and cached by default.cache: 'no-store'opts out of this caching for that specific fetch. You can also usenext: { revalidate: seconds }to set up Incremental Static Regeneration (ISR) at thefetchlevel. This means the data is fetched at build time (or on demand), cached, and then revalidated in the background after a specified interval. The RSC itself is still executed on the server for each request, but it uses the potentially stale, but fast, cached data.
The App Router’s core innovation is its ability to execute components as React Server Components by default, minimizing client-side JavaScript and enabling powerful data fetching patterns directly within your UI components.
This system is designed to bring server rendering closer to your components, letting you fetch data where you use it without complex data-fetching layers.
The magic of RSCs lies in their ability to execute only on the server and produce a serialized output that describes the UI. This means you can use Node.js APIs, fs, direct database connections, or any server-side logic directly within your components without shipping that code to the browser. The server renders the component, fetches the data, and sends back only the resulting UI structure and static content.
When you use use client, you’re essentially telling Next.js to treat that component and its children as traditional React components that will be rendered on the client. These client components receive props from their parent RSCs. The server renders the RSC, which might include a placeholder for a client component, and then the client-side JavaScript takes over to render and hydrate that client component.
The RSC payload is not HTML. It’s a JSON-like structure that React understands. For example, <h1>Hello</h1> might become {"type":"h1","props":{"children":"Hello"}}. This is what the client receives and uses to build the DOM. When a component fetches data, that data is embedded directly into this serialized output.
A common misunderstanding is that async/await in a component always means SSR. In the App Router, async/await in a component without 'use client' means it’s an RSC, and it runs on the server. If you have async/await within a 'use client' component, that code runs on the client after the component has been rendered and hydrated.
The next thing you’ll likely encounter is managing state and interactivity within Client Components and how they communicate with Server Components.