Skip to content
Dashboard

Common mistakes with the Next.js App Router and how to fix them

VP of Developer Experience

Link to headingUsing Route Handlers with Server Components

app/page.tsx
export default async function Page() {
let res = await fetch('http://localhost:3000/api/data');
let data = await res.json();
return <h1>{JSON.stringify(data)}</h1>;
}

Fetching JSON data from a Route Handler in a Server Component.

app/api/data/route.ts
export async function GET(request: Request) {
return Response.json({ data: 'Next.js' });
}

A Route Handler that returns static JSON data.

app/page.tsx
export default async function Page() {
// call your async function directly
let data = await getData(); // { data: 'Next.js' }
// or call an external API directly
let data = await fetch('https://api.vercel.app/blog')
// ...
}

Server Components are able to fetch data directly.

Link to headingStatic or dynamic Route Handlers

app/api/data/route.ts
export async function GET(request: Request) {
return Response.json({ data: 'Next.js' });
}

A Route Handler that returns static JSON data.

app/api/data/route.ts
export async function GET(request: Request) {
let res = await fetch('https://api.vercel.app/blog');
let data = await res.json();
return Response.json(data);
}

Return a list of blog posts as JSON data.

Link to headingRoute Handlers and Client Components

app/user-form.tsx
'use client';
import { save } from './actions';
export function UserForm() {
return (
<form action={save}>
<input type="text" name="username" />
<button>Save</button>
</form>
);
}

A form and input to save a name.

app/user-form.tsx
'use client';
import { save } from './actions';
export function UserForm({ username }) {
async function onSave(event) {
event.preventDefault();
await save(username);
}
return <button onClick={onSave}>Save</button>;
}

Server Actions can be called from event handlers.

Link to headingUsing Suspense with Server Components

app/page.tsx
async function BlogPosts() {
let data = await fetch('https://api.vercel.app/blog');
let posts = await data.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default function Page() {
return (
<section>
<h1>Blog Posts</h1>
<BlogPosts />
</section>
);
}

A page which contains an async component with data fetching.

app/page.tsx
import { Suspense } from 'react';
async function BlogPosts() {
let data = await fetch('https://api.vercel.app/blog');
let posts = await data.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default function Page() {
return (
<section>
<h1>Blog Posts</h1>
<Suspense fallback={<p>Loading...</p>}>
<BlogPosts />
</Suspense>
</section>
);
}

Using Suspense with React Server Components.

import { unstable_noStore as noStore } from 'next/cache';
async function BlogPosts() {
noStore(); // This component should run dynamically
let data = await fetch('https://api.vercel.app/blog');
let posts = await data.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

Opt-into dynamic rendering inside async components.

Link to headingUsing the incoming request

app/blog/[slug]/page.tsx
export default function Page({
params,
searchParams,
}: {
params: { slug: string }
searchParams: { [key: string]: string | string[] | undefined }
}) {
return <h1>My Page</h1>
}

Reading parts of the URL and the search parameters.

Link to headingUsing Context providers with App Router

app/theme-provider.tsx
'use client';
import { createContext } from 'react';
export const ThemeContext = createContext({});
export default function ThemeProvider({
children,
}: {
children: React.ReactNode;
}) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}

A Client Component that uses React Context.

app/layout.tsx
import ThemeProvider from './theme-provider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}

A root layout that weaves a client context provider and Server Component children.

Link to headingUsing Server and Client Components together

app/page.tsx
export default function Page() {
return (
<section>
<h1>My Page</h1>
</section>
);
}

A Server Component page.

app/counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

A Client Component button that increments a count.

app/page.tsx
import { Counter } from './counter';
export default function Page() {
return (
<section>
<h1>My Page</h1>
<Counter />
</section>
);
}

Using a Client Component from a Server Component.

app/page.tsx
import { Counter } from './counter';
function Message() {
return <p>This is a Server Component</p>;
}
export default function Page() {
return (
<section>
<h1>My Page</h1>
<Counter>
<Message />
</Counter>
</section>
);
}

Children of a Client Component can be Server Components.

app/counter.tsx
'use client';
import { useState } from 'react';
export function Counter({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{children}
</div>
);
}

The counter now accepts children and displays them.

Link to headingAdding “use client” unnecessarily

Link to headingNot revalidating data after mutations

app/page.tsx
export default function Page() {
async function create(formData: FormData) {
'use server';
let name = formData.get('name');
await sql`INSERT INTO users (name) VALUES (${name})`;
}
return (
<form action={create}>
<input name="name" type="text" />
<button type="submit">Create</button>
</form>
);
}

A Server Action that inserts the name into a Postgres database.

app/page.tsx
import { revalidatePath } from 'next/cache';
export default async function Page() {
let names = await sql`SELECT * FROM users`;
async function create(formData: FormData) {
'use server';
let name = formData.get('name');
await sql`INSERT INTO users (name) VALUES (${name})`;
revalidatePath('/');
}
return (
<section>
<form action={create}>
<input name="name" type="text" />
<button type="submit">Create</button>
</form>
<ul>
{names.map((name) => (
<li>{name}</li>
))}
</ul>
</section>
);
}

Revalidating data inside of a Server Action.

Link to headingRedirects inside of try/catch blocks

app/page.tsx
import { redirect } from 'next/navigation';
async function fetchTeam(id) {
const res = await fetch('https://...');
if (!res.ok) return undefined;
return res.json();
}
export default async function Profile({ params }) {
const team = await fetchTeam(params.id);
if (!team) {
redirect('/login');
}
// ...
}

Redirecting from a Server Component.

app/client-redirect.tsx
'use client';
import { navigate } from './actions';
export function ClientRedirect() {
return (
<form action={navigate}>
<input type="text" name="id" />
<button>Submit</button>
</form>
);
}

Redirecting in a Client Component through a Server Action.

app/actions.ts
'use server';
import { redirect } from 'next/navigation';
export async function navigate(data: FormData) {
redirect('/posts');
}

A Server Action that redirects to a new route.

Link to headingConclusion

Ready to deploy?