server state and ui ownership

why

  • a display component that fetches becomes a second authority for remote facts.
  • a cache key copied into several components makes invalidation a guessing game.
  • transport policy hidden inside markup is where no one searches for it.
  • distinct owners give each concern — remote state, ephemeral state, rendering, transport — exactly one searchable home; stale copies of authority cannot exist because nothing copies authority.

rule

keep rendering, server state, client state, route composition, and transport construction in distinct owners.

ownership map

  • page.tsx owns route composition and visible layout.
  • a display component owns rendering and local interaction composition, never the authority for persistent data.
  • a query hook owns network-derived state, cache identity, invalidation, and the typed boundary between transport output and UI consumption.
  • context owns shared ephemeral UI state only when a parent must coordinate descendants: open state, temporary selection, a short-lived interaction.
  • a server-side service owns composition through the generated client; a route boundary validates/authenticates then delegates to it.

trace one change through its owner

when a screen needs a new fact, begin at the contract, not with a fetch inside the component:

  1. define the consumer-shaped response and its authority in the API contract.
  2. regenerate the typed client from that contract.
  3. add or update the named query key and query hook.
  4. map its typed state to a semantic display component.
  5. compose that component visibly in page.tsx or the route layout.

cache key, authorization boundary, failure state, and rendering location stay individually searchable; a display change cannot silently take transport policy.

decision boundaries

concerncorrect ownerwhy
a server response that should survive navigationquery cacheit is remote authority, not local component memory.
a temporary choice shared by sibling controlscontexta parent coordinates it without turning it into durable data.
a request/response transformation with domain meaningservice or mapperit must be reusable independent of one rendered surface.
visible composition and route-level layoutpage.tsxa human can find the screen's main structure in one place.

do not

  • fetch persistent data in a display component.
  • put one-use page wrappers around a page only to hold its markup.
  • mirror query results into local state with an effect solely to make them renderable; that creates a second, stale authority.
  • put a route-specific cache key inline in multiple components.

verification

search the new public capability by its query-key symbol: one definition, one hook owner, deliberate invalidation callers. exercise loading, error, empty, and ready states without changing the component's source of truth.

good and bad

bad

interface WorkflowPanelProps {
workflowId: string;
}
function WorkflowPanel(props: WorkflowPanelProps) {
const [workflowRun, setWorkflowRun] = useState();
useEffect(() => void fetchWorkflowRun(props.workflowId).then(setWorkflowRun), [props.workflowId]);
}

the component owns transport and a second copy of remote authority: loading, error, and stale reads are untyped, cache identity is invisible to invalidation, and each sibling repeating the fetch drifts into its own disagreement about the same facts.

good

interface WorkflowPanelProps {
workflowId: string;
}
function WorkflowPanel(props: WorkflowPanelProps) {
const workflowRunQuery = useWorkflowRun(props.workflowId);
return <WorkflowRunState query={workflowRunQuery} />;
}

the query hook owns remote state; the component owns semantic rendering. cache identity has one named owner, invalidation is deliberate and searchable, and the component cannot grow a stale second authority because it never held one.

search pages

go to any page