A handful of ideas carry most of the App Router: nested layouts, Server and Client Components, Server Actions, rendering modes and metadata.
Layouts and pages
A layout receives its child route as children and wraps it. Any folder can have a layout, and layouts nest: the outer one wraps the inner one. Navigating between pages under the same layout keeps the layout and its state, so it is the place for navigation bars and sidebars.
// app/dashboard/layout.tsx
import Link from "next/link";
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<section>
<nav>
<Link href="/dashboard">Overview</Link>
<Link href="/dashboard/settings">Settings</Link>
</nav>
<div>{children}</div>
</section>
);
}
Server Components and Client Components
In the App Router, layouts and pages are Server Components by default. They run on the server and send only their result to the browser, so their code and imports never reach the client bundle, and they can query a database or use an API key safely.
When you need hooks such as useState or useEffect, event handlers such as onClick, or browser APIs such as window and localStorage, use a Client Component. Adding the "use client" directive at the top of a file puts that file, and the modules it imports, into the browser bundle.
// app/ui/like-button.tsx
"use client";
import { useState } from "react";
export function LikeButton({ initial }: { initial: number }) {
const [likes, setLikes] = useState(initial);
return <button onClick={() => setLikes(likes + 1)}>Likes {likes}</button>;
}
Server Components can import and render Client Components. Props passed across that boundary must be serializable (strings, numbers, plain objects, arrays and so on); functions cannot be passed, with Server Actions as the exception.
// app/posts/[id]/page.tsx (a Server Component)
import { LikeButton } from "@/app/ui/like-button";
import { getPost } from "@/lib/posts";
export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<LikeButton initial={post.likes} />
</article>
);
}
The recommended pattern is to push the "use client" boundary as far toward the leaves of the tree as possible. Keep only the interactive pieces, such as a button or a form, on the client, and you ship less JavaScript. To render server-generated content inside a Client Component, pass it in as children.
Server Actions
A Server Action (more generally, a Server Function) is an async function marked with "use server". It is called from the browser but runs on the server, handling forms and mutations without a separate API route. Passed to a form's action prop, it works even before JavaScript loads.
// app/guestbook/page.tsx
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";
export default function GuestbookPage() {
async function addEntry(formData: FormData) {
"use server";
const message = String(formData.get("message") ?? "");
await db.entry.create({ data: { message } });
revalidatePath("/guestbook");
}
return (
<form action={addEntry}>
<input name="message" required />
<button type="submit">Sign</button>
</form>
);
}
Treat every Server Action as a public endpoint: check auth and validate input inside it.
Rendering: static, dynamic and streaming
Next.js decides, route by route, when HTML is produced.
Reading request-time information such as cookies(), headers() or search params, or reading uncached data, makes that part render at request time. Wrap slow parts in <Suspense>, or add a loading.tsx next to the page, and Next.js sends the rest of the page first and streams the remaining content as it resolves.
// app/dashboard/page.tsx
import { Suspense } from "react";
import { RecentOrders } from "./recent-orders";
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading orders...</p>}>
<RecentOrders />
</Suspense>
</main>
);
}
Recent releases also offer Cache Components (the cacheComponents option), which mixes a static shell with dynamic parts on one page. This area is evolving, so check the docs for your version.
Export a metadata object from a layout or page to set the title, description and Open Graph tags, or a generateMetadata function when they depend on data. Both are supported only in Server Components.
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { getPost } from "@/lib/posts";
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return { title: post.title, description: post.summary };
}
Summary
- Layouts nest and persist across navigation; pages provide the content for each URL.
- Components are Server Components by default; split out only the interactive parts with
"use client". - Server Actions handle forms and mutations on the server, but they still need auth checks and input validation.
- Routes render statically or dynamically, and
Suspense or loading.tsx enables streaming. - Use
metadata and generateMetadata to give every page proper search information.
0 comments
Sign in · Sign in to leave a comment.