Next.js

Verified against Claude Code · 2026-07-22

Decide exactly where the client/server boundary belongs in your component tree

Reviews a component tree and pushes every 'use client' directive down to the smallest leaf that actually needs it, with a named trigger for each boundary instead of a gut-feel call.

Claude CodeCursorGitHub Copilot4 fillable variables

The prompt

Ready to copy — highlighted parts are example details you can swap.

You are auditing a Next.js App Router component tree to decide, component by component, whether each one should stay a Server Component (the default, no directive) or become a Client Component ('use client'). The goal is to push every client boundary down to the smallest leaf that genuinely needs it, never up to a shared ancestor for convenience.

COMPONENT TREE
app/checkout/page.tsx renders <OrderSummary>, <PromoCodeForm>, <ShippingAddressPicker>, and <PaymentButton>, all currently imported into one page component marked 'use client' at the top.

WHAT ACTUALLY NEEDS TO RUN IN THE BROWSER
PromoCodeForm needs an input and an apply button with pending state; ShippingAddressPicker needs a client-side address autocomplete widget; PaymentButton opens a third-party payment SDK modal on click.

CURRENT BOUNDARY PLACEMENT
The entire page.tsx has 'use client' at the top, so OrderSummary re-fetches and re-renders on the client via useEffect instead of being server-rendered with the order data it already had at request time.

REACT VERSION
19

DECISION RULES
Mark a component 'use client' only if it directly does at least one of the following: calls a stateful hook such as useState, useReducer, or a hook that reads a browser API; attaches a real event handler like onClick, onChange, or a client-side onSubmit; reads window, document, localStorage, navigator, or any other browser-only global; imports a third-party library that itself requires the client runtime, such as most charting libraries, rich-text editors, and drag-and-drop libraries; or consumes a React Context created with createContext for a value that changes at runtime. Every other component defaults to a Server Component, including anything that only fetches data, reads environment variables or secrets, or renders markup with no runtime behavior of its own, regardless of how deep it sits in the tree.

When a single component needs both server-fetched data and client interactivity, do not resolve the tension by making the whole thing a Client Component and fetching data with useEffect instead. Split it: keep the data fetch in a Server Component parent or grandparent, and pass the fetched data down as props or children into a small Client Component that owns only the interactive slice. Never move the 'use client' boundary higher in the tree than the smallest node that actually needs it, because everything imported beneath a client boundary in the module graph ships to the browser as JavaScript, including modules that look server-safe on their own — a chart library imported by a client-marked parent bundles into the client even if the specific component rendering the chart does nothing browser-specific itself.

If a component that is already marked 'use client' today does not actually meet any trigger in the rules above, treat that as a real finding, not a stylistic nitpick. Name what it currently costs — a heavier client bundle, a subtree that could have streamed server-rendered HTML but instead ships as an empty shell waiting for hydration — and recommend converting it back to a Server Component rather than leaving it as a safe-looking default.

OUTPUT FORMAT
A table: Component | Current marking | Recommended marking | Trigger from the rules that justifies Client (or "none — revert to Server") | One-line reason. Then, for every component you would restructure by splitting a mixed component into a server parent plus a client leaf, show the before/after boundary as a short tree diagram, not full code. Close with one paragraph describing what actually changes in the client JavaScript bundle if every recommendation here is applied, naming which specific dependency stops shipping to the browser.

Customize

Optional — swap in your own details for the highlighted parts above.

Why this works

"use client" is not a per-component opt-in evaluated in isolation — it marks a boundary in the module graph, and every module imported beneath that boundary ships to the browser as JavaScript regardless of whether that specific module does anything browser-specific on its own. A generic "is this interactive?" review tends to mark parents defensively, which drags entire subtrees — including server-safe children imported alongside the one component that actually needs the browser — into the client bundle without anyone deciding that on purpose. Giving the model a closed, checkable list of the actual triggers that require 'use client' turns a vague architectural judgment call into a per-component checklist it can apply consistently instead of pattern-matching on "this looks like it might need state." The explicit split-don't-lift rule targets the single most common App Router mistake directly: a page gets marked 'use client' because one child has an onClick handler, and everything else in that page — including data-fetching logic that worked fine as a plain async function — gets rewritten around useEffect and a loading state that didn't need to exist. Requiring a finding, not a shrug, when an existing client boundary doesn't meet any real trigger matters because these boundaries accumulate silently: a component gets marked 'use client' once for a reason that gets refactored away later, and nothing in a normal code review flags that the directive is now dead weight, since the code still runs correctly either way — it just runs in the wrong place, shipping bytes to every visitor's browser for no behavior anyone can observe. Naming the actual bundle consequence at the end, rather than stopping at "this should be a Server Component," is what makes the recommendation actionable to someone who has to justify the change in a PR — "remove this boundary" is a stylistic ask, while "this stops shipping a 40KB charting library to visitors who never open the chart panel" is a reviewable claim with a real, measurable payoff attached to it.

What you get back

Component | Current | Recommended | Trigger | Reason OrderSummary | Client (inherited) | Server | none | Only renders order data, no hooks or handlers of its own PromoCodeForm | Client | Client | useState + onSubmit | Owns the input value and the pending state of the apply button ShippingAddressPicker | Client | Client | third-party autocomplete library | The autocomplete widget itself requires the browser PaymentButton | Client | Client | onClick opens a client-only SDK modal | Correctly scoped already — no change needed Restructure: split the page into a Server Component that fetches the order and renders <OrderSummary> directly, wrapping only <PromoCodeForm>, <ShippingAddressPicker>, and <PaymentButton> in their own small client leaves. Bundle impact: the payment SDK and the autocomplete library still ship (they're genuinely needed), but OrderSummary's rendering logic and any data-formatting helpers it used no longer cross into the client bundle at all.

Verified against

Claude Code Sonnet 4.6 · 2026-07-22

Cursor Cursor 2.1 · 2026-07-29

Changelog

  • 2026-07-22 Initial publish, verified against Claude Code (Sonnet 4.6) and Cursor 2.1 on Next.js 16 App Router projects.

Need this built into your business?

If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.

EXPLORE CUSTOM SOFTWARE
All Next.js prompts

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY