Skip to content
Design Engineering
Architecture

Where state belongs

Choose between local, URL, shared, and server state based on how the information is used.

Why this matters

State is info that can change. Put it where its owner, lifespan, and sharing point. A crew member's expanded assignment row lives somewhere different from tomorrow's saved roster. Get this wrong and you maintain two copies of one fact — then debug why they disagree.

What to understand

Start with lifetime and authority: one interaction stays local, a draft belongs to the form, shareable view belongs in the URL, saved records belong to the server with an intentional client copy, a shared preference belongs in a scoped provider. Don't push every shared value into a global store — a prop, a nearby parent, or a computed value can be enough.

Copying server results into a global store makes a second saved copy to keep fresh. Keep the server record in the query layer and model only the person's unsaved draft apart from it. Login info may show through context, but permission is still checked on the server.

Watch for

  • Every shared value pushed into a global store by default.
  • A stored total drifting from the selection it was computed from.
  • Server results copied into a store — a second saved copy to keep fresh.
  • Permission trusted from a browser store anyone can edit.
  • Several booleans claiming opposite things at once.
  • Secrets or sensitive drafts in URL or browser storage.
  • Resetting only on unmount instead of on ownership change.

Strong default

Ask your agent: "can this be a prop or a computed value instead of store state?" Compute what derives, keep one clear status instead of tangled booleans, and reach for a reducer or state machine only when steps need named events.

When this doesn't apply

A two-way toggle doesn't need a state machine. Saved data surviving reload is a choice, not a default — a deep link leaks through history, logs, and shares, and browser storage outlives the session with any script able to read it.

In practice

StateTypical homeWhy
An open disclosureLocal component stateIt belongs to one interaction.
Fields being editedA form or feature state modelThey are a draft, not yet the saved record.
Non-sensitive filters or page numberURL search parametersReload, sharing, and back/forward should restore the view.
Saved projects or permissionsServer, with an intentional client cacheThe browser holds a copy, not the authority.
Theme or a shared interaction preferenceA scoped provider or storeMultiple components need one consistent value.

If a total comes from selected crew, compute it. Storing both invites drift: removing a person must then update two values.

If this project uses typed search-parameter helpers — nuqs on Next.js, validateSearch on TanStack Start — filters and page live in the URL, so reload and sharing restore the view for free, while the roster itself stays in the query layer and is never copied into a store. Ask your agent: "which of these values survives reload, and which is recomputed?"

A write has distinct outcomes
type SaveState =
  | { status: 'idle' }
  | { status: 'saving'; operationId: string }
  | { status: 'saved'; projectId: string }
  | { status: 'rejected'; message: string }
  | { status: 'unknown'; operationId: string };

“Unknown” means the response was lost and the save may have landed. Check the result before sending a new save. The type states the UI agreement; the server must back the lookup.

For a multi-step form, decide what survives going back, reloading, signing out, and switching crews. Reset when ownership changes — new crew, new user — not just when a handy component unmounts.

Verify

Walk through opening, editing, saving, failing, cancelling, and returning where they apply. A screenshot shows a state; replaying the task proves how someone gets there and out.

Use $flow to map transitions and data fetching for server-state freshness.

Last updated on

On this page