Blog post
The Hardest Part of Next.js Was Learning Where State Should Not Live
Why server-first Next.js development is less about state management libraries and more about putting authority in the right place.
The Hardest Part of Next.js Was Learning Where State Should Not Live
“Where should this state live?” sounds like a storage question.
In a server-first application, it is usually an authority question.
Who is allowed to decide which tenant is active? Who can see this record? Which permissions apply? What changed after a mutation? Which value is official when the browser and server disagree?
Those are not questions a state library can answer. They are questions about which layer is trusted to establish reality.
The category that causes the confusion
Calling all of these things “state” hides an important difference.
A dropdown is open. A form field is being edited. A modal is visible. A tab is selected. The browser owns these interactions because they describe what the user is doing with the interface right now.
A record’s access policy, a tenant boundary, a saved value, or the result of a mutation is different. Those values have authority behind them. They must be validated, scoped, persisted, and refreshed according to rules the browser cannot be allowed to invent.
Both categories change over time. Only one is primarily a local interaction concern.
The mistake is not putting a value in a client component. The mistake is allowing a client component to become the source of truth for a decision it does not own.
Why AI makes the old default persistent
A familiar React pattern is easy to recognise:
useEffect(() => {
fetch('/api/data').then(...)
}, [])
AI has seen this pattern everywhere. Given a component and a request for data, it may produce client-side fetching, local mirrors of server values, optimistic updates, and a loading state without first asking who owns the data.
The code can be valid. The page can render. The pattern can still be wrong for a server-first application.
A client mirror creates another version of the truth. It can go stale. It can be scoped incorrectly. It can make permissions appear to be a display concern. It can place application logic in a component because the component is where the request happened to begin.
This is pattern completion, not malicious behaviour. The architecture has to make the correct default easier to follow than the familiar one.
Server authority changes the default
A useful server-first default is straightforward:
- fetch authoritative data on the server;
- keep secrets and access decisions on the server;
- establish identity and tenant scope from trusted request context;
- render the initial view from server truth;
- let client components exist where interaction requires them;
- treat the client as a participant in a workflow, not the owner of durable reality.
This is not a claim that server code is automatically safe. It is a placement rule: the layer that can enforce the invariant should own the decision.
Mutations make the distinction more visible. A form submission is not merely a click that updates local state. It may validate input, check permissions, execute a use case, change durable data, invalidate caches, and return a new representation of the world.
The client captures intent. The server validates and executes. The application layer owns the workflow. The UI reflects the result.
That sequence is easier to review because it gives each layer a responsibility instead of making every component a partial authority.
Freshness is part of state management
Server-first design also changes what “state management” includes.
The question is not only where a value is stored. It is whether the value is fresh for the right user and tenant after a mutation.
Which data may be cached? Which route or component depends on it? What must be revalidated after an update? Can a cached result cross a scope boundary? What does the UI show while the authoritative operation is still pending?
AI can generate a page that works on the first request without answering those questions. A working page is not enough if it shows yesterday’s permissions or another tenant’s cached result.
Cache invalidation is therefore not an implementation footnote. It is part of deciding where authority lives and how the rest of the system learns that authority has changed.
The model’s limit
There is an understandable overcorrection: if client state creates confusion, move everything to the server.
That is also wrong.
The browser should own interaction state. A modal’s visibility, an in-progress draft, a selected tab, and the local shape of an input do not need a round trip to become legitimate. Forcing those details into server state makes the interface slower and the architecture less honest about what is actually authoritative.
The useful boundary is not “server good, client bad.” It is “interaction is local; authority is deliberate.”
That was the harder Next.js lesson. State management was not mainly a choice between libraries, hooks, or stores. It was learning to stop assigning durable decisions to a layer that can only display or guess them.
The browser is excellent at expressing intent.
The server is where the product decides what that intent is allowed to change.
Continue exploring
Follow the same line of thought through themes, tags, or a broader local search across the archive.
Keep following the thread.
AI Made UI Drift Cheaper
Why AI does not break product coherence by itself, but makes local UI deviation cheap enough that drift appears faster than many teams can review it.
I Could Build Faster. That Was Not the Hard Part.
AI makes software cheaper to explore and build, but not equally cheap to understand, secure, review, and own.
Local Knowledge Beats Always-On Tools in AI-Assisted Development
Why the biggest gain in AI-assisted development comes from durable local knowledge and disciplined workflow structure, not from keeping every external tool always active.