Skip to content
Design Engineering
Product Behaviour

Loading and updating data

Decide where data loads, when to refresh it, and how to handle failed requests.

Why this matters

A data-loading choice is an experience choice: when can a crew member see tomorrow's assignment, how fresh is it, and what still works if the service fails?

What to understand

Load the first view on the server when your framework and product benefit from it. Use a client query layer for things the browser drives: filters, refresh, live updates, or shared client copies. Don't fetch the same thing in several places with no plan for what's fresh.

A cache is saved copies that may be out of date. For each resource, name the scope, acceptable age, refresh trigger, and what a save invalidates. Keep one person's data scoped to their identity and crew; clear or replace client copies when that context changes.

Watch for

  • The same thing fetched in several places with no freshness plan.
  • A cache key missing a filter and reusing the wrong results.
  • A secret included in a cache key, leaking through tooling and logs.
  • Freshness guessed from where a component runs instead of set deliberately.
  • HTTP errors trusted because fetch resolved.
  • One boolean standing in for first-time loading and background refresh.

Strong default

Put the read near its owner: server for the first view, client query layer for browser-driven needs. Prefer your framework's data path or an existing query library when cancelling, races, loading, errors, and saved copies repeat. Set cache policy deliberately per resource.

When this doesn't apply

Effects can fetch, but then you own cancelling, races, loading, errors, and saved copies. That's a tradeoff, not a ban on effects. Current Next.js cache behavior depends on the API and configuration. Set policy deliberately and check the installed version's caching documentation; don't guess freshness from whether a component runs on the server.

In practice

A project list key might include organization, filter, and page. Leaving out a filter can reuse the wrong results. Including a secret in the key can leak it through tooling and logs. Ask your agent: "what exactly makes up this key, and what happens when the crew or filter changes?"

Browser fetch resolves for HTTP error responses such as 404 or 500. Check status and validate external data before trusting it.

Illustrative read boundary
async function readProjects(signal?: AbortSignal) {
  const response = await fetch('/api/projects', { signal });
  if (!response.ok) throw new Error('Projects could not be loaded'); 
  const data: unknown = await response.json();
  return projectListSchema.parse(data);
}

projectListSchema represents your actual runtime schema; this is a boundary sketch, not a complete data layer. Keep raw server diagnostics out of user-facing messages.

SituationUseful behavior
No data has loaded yetShow a bounded loading state without inventing results.
First load failsExplain what could not load and offer a meaningful recovery.
Loaded data is refreshingKeep useful content visible; indicate pending refresh if relevant.
Refresh failsKeep stale content only when safe, with freshness and recovery made clear.
No records existExplain the next useful action.
A filter matches nothingPreserve the filter and provide a way to adjust or clear it.

For TanStack Query, tell first-time loading apart from background refresh. Use your installed version's state model rather than one boolean for both.

If this project uses TanStack Start, the split is explicit: the route loader fetches the first view on the server with the crew's identity attached, and Query owns everything after — filters live in the query key (say ['assignments', { crew, status }]), refetch happens on refocus, and a save invalidates the assignments key. Ask your agent: "which key changes when the filter changes, and what invalidates it after a save?"

Optimistic updates show the expected result before it's confirmed. Use them when the action is easy to undo and conflicts are manageable. Keep a snapshot or fallback plan, cancel clashing reads when it helps, and replace the guess with the server's answer.

Pay, stock, and access changes usually need a confirmed result. In every case, a timeout leaves the outcome unknown — it may have saved. Agree on safe retry (doing it twice has the same effect as once) before you retry automatically. Ask your agent: "what makes this retry safe?"

Verify

First load, background refresh, failed refresh, empty, and filter-matched-nothing — each with its recovery and freshness made clear. Retry safety agreed before automatic retry.

Use $states to review this lifecycle in an existing screen. Continue with error handling.

Last updated on

On this page