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.tsxowns 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:
- define the consumer-shaped response and its authority in the API contract.
- regenerate the typed client from that contract.
- add or update the named query key and query hook.
- map its typed state to a semantic display component.
- compose that component visibly in
page.tsxor 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
| concern | correct owner | why |
|---|---|---|
| a server response that should survive navigation | query cache | it is remote authority, not local component memory. |
| a temporary choice shared by sibling controls | context | a parent coordinates it without turning it into durable data. |
| a request/response transformation with domain meaning | service or mapper | it must be reusable independent of one rendered surface. |
| visible composition and route-level layout | page.tsx | a 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.