The rules behind the bugs
Most Next.js gotchas come from the server/client boundary, caching, request-time APIs and special errors. Name the rule, then the fix.
- BoundaryHooks, window and hydration.
- CachingStale data and unexpected dynamic routes.
- APIsAsync params, env vars and redirect().
Next.js has a handful of rules that cause most real-world bugs — and most tricky interview questions. Each one below shows the code, what actually happens and the rule behind it.
1. What happens? (useState in a page without 'use client')
A build error: hooks like useState and useEffect only work in Client Components, and pages are Server Components by default. Move the interactive part into a small component marked 'use client'.
// app/page.tsx
import { useState } from 'react';
export default function Page() {
const [n, setN] = useState(0); // Error: useState only works in Client Components
return <button onClick={() => setN(n + 1)}>{n}</button>;
}2. Why does this cause a hydration error?
The server renders one time value and the browser renders another, so the HTML does not match. Render time-dependent or random values only on the client (inside an effect), or pass the value from the server, or use suppressHydrationWarning for things like timestamps you accept will differ.
export default function Clock() {
return <p>{new Date().toLocaleTimeString()}</p>; // differs between server and browser
}3. Why does the list still show old data after my Server Action?
The page output was cached and nothing told Next.js to refresh it. Call revalidatePath('/todos') or revalidateTag('todos') in the action after the change (or router.refresh() on the client).
4. Why is my API key undefined in the browser?
Environment variables without the NEXT_PUBLIC_ prefix are server-only — by design. If it is a secret, keep the call on the server (Server Component, action or route handler). Only non-secret values should be public.
console.log(process.env.STRIPE_SECRET_KEY); // undefined in client components (good!)
console.log(process.env.NEXT_PUBLIC_SITE_URL); // available in the browser5. Why does redirect() inside try/catch not work?
redirect() and notFound() work by throwing a special error. A surrounding catch swallows it. Call them after the try/catch, or rethrow errors you do not handle.
'use server';
export async function save(formData: FormData) {
try {
await db.item.create({ data: parse(formData) });
redirect('/items'); // caught below — the redirect never happens
} catch (e) {
return { error: 'Could not save' };
}
}
// Fix: move redirect('/items') below the try/catch.6. Why is my page dynamic when I expected it to be static?
Something in the tree reads request data — cookies(), headers(), searchParams — or fetches without caching. One dynamic read (even in a layout) makes the route dynamic. Move personalised parts into a Suspense-wrapped component or cache the data explicitly.
7. Why does accessing params.slug fail in Next.js 15+?
params is a Promise now; reading params.slug directly gives undefined (and a warning). Await it first.
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params; // correct in Next.js 15+
}8. Why does my window/localStorage code crash during rendering?
Client Components are still pre-rendered on the server, where window does not exist. Access browser APIs inside useEffect or event handlers, or load the component with next/dynamic and ssr: false.
'use client';
export function Theme() {
const [theme, setTheme] = useState('light');
useEffect(() => { setTheme(localStorage.getItem('theme') ?? 'light'); }, []); // runs only in the browser
return <span>{theme}</span>;
}Remember
Key points
- Hooks need 'use client'
Pages are server components.
- Same HTML on server and client
Or hydration fails.
- Revalidate after mutations
Or users see old data.
- redirect() throws
Keep it outside try/catch.
Interview drill
Follow-up questions
What the interviewer asks after your first answer. Turn on practice mode, answer each one aloud, then reveal.
-
1 Why does a <div> inside a <p> cause a hydration error? Intermediate
It is invalid HTML; the browser fixes it differently from React's output, so the trees do not match.
-
2 Can a Client Component be async? Basic
No — async components are Server Components only; in client code fetch in effects or with use() and Suspense.
-
3 Why does console.log in a Server Component not show in the browser? Basic
It runs on the server, so the log appears in the terminal or server logs.
-
4 What does 'use client' at the top of a file affect? Intermediate
That module and everything it imports become part of the client bundle; it marks a boundary, not just one component.
-
5 Why can't I pass a function from a Server Component to a Client Component? Intermediate
Props crossing the boundary must be serialisable; functions are not — except Server Actions, which are passed as references.
-
6 Why is useSearchParams forcing client-side rendering up to the nearest Suspense? Advanced
Search params are known only at request time in the browser for static routes; wrap the component using it in Suspense so the rest stays static.