Pitfalls
The mistakes that happen once in almost every Next.js project
Eight patterns that keep coming up – each with the thinking error behind it and the shortest fix.
If you have read the previous lessons you know the building blocks. This one collects the cases where they get assembled wrongly in everyday work.
1. “use client” at the top of the page
"use client";
export default function Page() {
const [open, setOpen] = useState(false);
return (
<>
<HugeList />
<button onClick={() => setOpen(true)}>Filter</button>
</>
);
} export default async function Page() {
const data = await load();
return (
<>
<HugeList data={data} />
<FilterButton /> {/* only this file is client */}
</>
);
} Why it happens: the error message points at the page, so that is where the line goes. The right move is to extract the interactive part.
2. Calling your own API from your own server
// in a server component
const res = await fetch("https://my-site.com/api/posts");
const posts = await res.json(); // in a server component
const posts = await db.post.findMany(); A network detour to yourself, plus serialisation to JSON and back. Route handlerA `route.ts` that answers an address without being a page – your way to a custom API.→ exist for access from outside.
3. Investigating caching in development mode
Almost nothing is cached there. To find out whether a page goes static you need npm run build && npm run start – and the symbol column of the build output.
4. Using params directly
export default function Page({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>;
} export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
} The same applies to searchParams, cookies() and headers(). Older examples online still show the direct access.
5. A server action with no permission check
"use server";
export async function remove(id: string) {
await db.post.delete({ where: { id } });
} "use server";
export async function remove(id: string) {
const user = await currentUser();
if (!user?.isAdmin) throw new Error("Not allowed");
await db.post.delete({ where: { id } });
} Every action gets a public endpoint. The fact that the button is only shown to admins protects nothing.
6. Not revalidating after writing
"use server";
import { revalidatePath } from "next/cache";
export async function create(formData: FormData) {
await db.post.create({ data: … });
revalidatePath("/blog"); // the overview
revalidatePath("/"); // in case the latest are shown there
} 7. Putting everything in one Suspense area
<Suspense fallback={<Spinner />}>
<Post />
<Comments /> {/* only this one is slow */}
<Recommendations />
</Suspense> <Post />
<Suspense fallback={<CommentSkeleton />}>
<Comments />
</Suspense>
<Recommendations /> Otherwise the fast content waits for the slow one – exactly what streaming was meant to prevent.
8. useEffect for data that exists on the server
"use client";
function List() {
const [data, setData] = useState([]);
useEffect(() => {
fetch("/api/posts").then(r => r.json()).then(setData);
}, []);
…
} export default async function List() {
const data = await db.post.findMany();
…
} The second route saves the API, the loading state, the race – and the content is in the HTML straight away.
▸ In more depth: a debugging checklist optional
Page shows stale data
1. npm run build – does it say ○ where you expected ƒ?
2. Hard reload: fresh? → the router cache in the browser
3. Fresh after a new build? → revalidate is missing
4. Still stale? → the data cache, check revalidateTag
"needs useState" during the build
→ a hook in a server component.
Extract the interactive part; do not mark the page.
Bundle suddenly much larger
→ a client boundary slipped upwards.
Compare "First Load JS" in the build output.
Server action does nothing
→ is revalidatePath missing? Or did it throw instead of
returning an error value? Most of these cases are visible in the npm run build table before you open a single file. A quick glance at it on every bigger change saves long searches.
Does it stick?
6 questions on this lesson. Wrong answers show up in your stats.