Fixing Claude Code's "use client" mistakes in the Next.js App Router
The most common App Router build and runtime errors come from the server/client split, and AI assistants hit them often. The good news is that they are mechanical enough to detect by reading the file after each edit.
The mistakes
These are the patterns that account for most of the breakage:
- Client features in a Server Component. React state, effects or event handlers in a file with no
"use client". The build error reads like "You're importing a component that needs useState". "use client"in the wrong place. The directive placed below the imports. Next.js ignores it there, so the file is treated as a Server Component.- Server-only code in a client file.
server-onlyornext/headersimported into a client component. metadataexported from a client component.metadataorgenerateMetadatain a client file is rejected at build time.- Async client components.
export default async functionin a client component is not supported.
Why catching it at edit time helps
If you only find these at next build, you pay a round trip: build, read the error, ask Claude to fix it. A PostToolUse hook that runs right after an edit lets Claude see the problem in the same turn.
The mechanism is simple. Claude Code runs your script after a file edit and passes the tool call as JSON. If the script exits with code 2, its message goes back to Claude as feedback. The message should be specific: say what is wrong, in which file, and the fix ("add "use client" as the first line" or "move metadata to a Server Component layout").
What the check looks at
Keep it text-based and scoped:
- Only files under
app/orsrc/app/. Whether code elsewhere is client or server depends on who imports it, so a text check cannot judge it. - Look for hook calls such as
useState(and event props such asonClick={in files lacking the directive. - Check that
"use client"is the first statement, not below an import. - Look for the server-only imports and the
metadataand async-default-export cases listed above.
// sketch of one rule
const usesClientFeatures = /\buse(State|Effect|Reducer)\(|\bon[A-Z]\w+=\{/.test(src);
const hasDirective = /^\s*["']use client["']/.test(src);
if (usesClientFeatures && !hasDirective) {
console.error(`${file}: uses client-only features but has no "use client" at the top.`);
process.exit(2);
}
Expect some false positives
Because the check is text-based, it can complain about a file that is fine, for example a Server Component that contains useState( inside a code sample string. Move the sample to a separate file or add that path to a skip list at the top of the loop. Files outside app/ are not checked at all, which is deliberate.
Prevent it at the source too
Hooks catch mistakes after the fact. Rules in CLAUDE.md reduce how often they happen. Short, checkable rules work better than general advice: when to add "use client", where metadata lives, how NEXT_PUBLIC_ variables are handled, and a requirement to state the caching choice, since caching defaults differ between Next.js major versions.
For larger work, a planning step helps. Deciding server versus client, data, caching, auth and error states before any code is written avoids the "write first, restructure later" cycle.
Going further
The Next.js Claude Code workflow pack includes this client-boundary hook, a route-planning subagent and a CLAUDE.md template written as short checkable rules.
This article was written with AI assistance.