skip to content

Remote Data

The life of server-owned data in a component tree: when a request starts, what renders meanwhile, which response lands, how it is cached, how a write goes back. Most screen bugs are data-timing bugs.

on this pageshow

explore

questions

page 1 of 2

Which on-screen states must a component that loads data from a server render, besides the successful result?

level: juniorimportance: must knowfreq 78%

answer

  1. more than loading and loaded
  2. one branch per outcome
  3. empty is not a failure
  4. two flags, six situations
  5. one named status, not booleans

basics

~20 s

Besides the data, a remote-data component needs branches for idle, loading, failure, and a successful response that carried nothing. Each is a different sentence to the user; collapsing them into one blank region is the usual bug.

solid answer

~40 s

A request is a small state machine, so the component renders one branch per state rather than data-or-spinner. The states worth a branch are **idle** (nothing asked for yet, which only exists when the user triggers the request), **loading** with nothing to show yet, **ready** with data, **empty** — a perfectly good response that carried no items — and **error**. A sixth condition matters once anything is on screen: **refetching**, a request in flight while a previous value is still displayed, which should keep the content and hint quietly rather than fall back to the loading branch. Modelling this as one named status is safer than separate `loading` and `error` booleans, because two booleans describe four combinations and nothing stops the impossible ones being set.

code

pseudocode · 24 lines
pseudocode
state status = "idle"      # idle | loading | refetching | ready | empty | error | stale
state items  = none
state failure = none

action load(params):
    if items is none: status = "loading"     # first load only
    else:             status = "refetching"  # keep what is on screen
    result = request(params)
    if result.failed:
        failure = result.failure
        status  = if items is none then "error" else "stale"
    else:
        items  = result.items
        failure = none
        status = if items.count == 0 then "empty" else "ready"

render:
    when status is "idle"       -> prompt: what this will show, and how to start
    when status is "loading"    -> placeholder shaped like the coming content
    when status is "empty"      -> "nothing here yet" + the action that creates one
    when status is "error"      -> failure message in this region + retry trigger
    when status is "stale"      -> items + non-blocking failure notice + retry trigger
    when status is "refetching" -> items + a quiet in-progress hint
    when status is "ready"      -> items

go deeper

for a junior

Recall the list: nothing requested yet, in flight, succeeded with data, succeeded with nothing, failed. Then check your own component actually renders something different for each of them.

for a middle

Explain why three facts produce six situations, and why a derived status removes the impossible ones. Be able to say what each branch should reserve in layout and what it should offer the user.

for a senior

Show the judgment: which regions may be replaced by a placeholder, which must keep their previous value, and how failure is surfaced without taking down the surrounding screen. Point at the review habits that catch a missing branch.

for a principal

Frame it as a contract every data-backed surface in the app owes, and decide how much of it is enforced by shared components versus left to each team. The cost of no convention is inconsistent copy and screens that jump.

## A request is a state machine, not a boolean Server-owned data arrives after the first render, so a component that depends on it renders at least once without it. What it puts on screen in that gap — and in the gaps that come later — is not a detail of the request; it is most of what a user experiences as speed and reliability. The conditions that deserve their own rendered branch are: 1. **Idle** — nothing has been requested yet. This state only really exists when the request is triggered by the user: a search that runs on submit, a panel that loads when it is opened, a report that loads when a date range is chosen. A component that requests as soon as it mounts passes through idle too fast to be worth designing for. 2. **Loading, first time** — a request is in flight and there is no previous value at all. This is the state where a placeholder costs nothing, because there is nothing on screen to preserve. 3. **Ready** — a response arrived and it has something to show. 4. **Empty** — a response arrived, it was entirely successful, and it carried no items. Not a failure. Worth noting that emptiness is *derived* from the payload rather than reported by the request: the request has succeeded. It earns a branch because the user needs a different sentence, not because the transport did something new. 5. **Error** — no usable data: the request never reached the server, the server refused it, or the payload could not be understood. 6. **Refetching** — a request is in flight *while a previous value is still on screen*. Most screens spend more time here than in first-load, because of polling, revalidation, and parameter changes. ## The two-boolean trap Teams usually start with `loading` and `error` flags plus the data, then discover that the three of them encode more situations than the render code handles: | `loading` | `error` | value held | What the component should actually render | |---|---|---|---| | true | none | none | first-load placeholder in the shape of the coming content | | true | none | present | the previous value, plus a quiet in-progress hint | | false | set | none | error message, in this region, with a retry trigger | | false | set | present | the previous value, plus a non-blocking failure notice | | false | none | none | the empty branch — the response arrived with nothing | | false | none | present | the data | Two flags do not make two branches; three facts make six. Code written as `if (loading) … else if (error) … else render(data)` silently merges the last two rows, so a successful empty response renders as a bare region with no explanation, and a failure that leaves the flag set in only one path leaves a placeholder spinning forever. ## What each branch owes the user - **Idle** — say what will happen and how to start it, never a placeholder for unstarted work. - **Loading** — reserve roughly the space the content will occupy, so the page does not jump when it lands. Shaped placeholders do that better than a small centred indicator, at the cost of being wrong when the real content is a different size. - **Empty** — name the reason and offer the next step, which differs sharply between "you have not created anything yet" and "your filter excluded everything". - **Error** — describe the failure in the user's terms, keep it inside the region that failed, and offer a way to try again. Raw failure text and technical codes are not a message. - **Refetching** — keep the content, indicate progress subtly, and do not move layout. Replacing readable content with a placeholder on every refresh is experienced as the screen breaking. ## One status value instead of flags Deriving a single named status — and reading the branch from it — removes the impossible combinations by construction, and makes the empty case impossible to forget because it must be named. The plumbing differs by reactivity model: a runtime that re-runs the component function on every write recomputes the branch as part of that re-run; a fine-grained runtime recomputes only the derived status and swaps the branch; a compile-time runtime resolves the branch structure ahead of time and patches in place. The set of states, and the obligation to render each one, is identical in all three. ## Where the branches live A tree can express pendingness two ways: each component renders its own branch, or a subtree declares that it cannot render yet and one ancestor boundary renders a single fallback for all of it. That choice changes how many placeholders a user sees; it does not change the list above, because something still has to render empty and failed. ## How this shows up in review - A component with a spinner branch and a data branch, and nothing else. - An empty list rendered as a successful nothing, with no copy at all. - A failure handler that substitutes an empty collection, turning every failure into an empty state. - A refresh that blanks a region the user was reading.

  • Why is one named status usually safer than separate loading and error booleans?
    Two booleans describe four combinations, most of which are impossible, and nothing in the type prevents setting them. A single status can hold only one value at a time, so "loading and failed" cannot be represented, and the empty case has to be named rather than falling out of an else branch by accident.
  • Is the idle state worth modelling for a component that requests as soon as it appears?
    Rarely. If the request starts on mount, the first render is already the loading state and idle is unobservable. Idle earns its branch when the user triggers the request — a search before the first submit, a lazily opened panel — because something has to be on screen that is not a placeholder for work nobody started.
  • Should the loading branch always replace the content it is standing in for?
    Only when there is no content to keep. With a previous value on screen, replacing it is a downgrade: the user loses what they were reading and the layout moves twice. Keep the value, mark that a request is in flight, and reserve the placeholder for the case where the region is genuinely empty.

saying these in an interview costs you the question

  • Thinks a loading flag plus the data covers every case
  • Renders a successful empty response as a blank region with no message
  • Treats a response with no items as a failed request
  • Leaves a placeholder on screen forever because no branch handles failure
  • Keeps loading and error booleans that can both be true at once
  • Catches the failure and substitutes an empty collection
open as a page

If a component starts its data request only after it has rendered once, what does the user wait for?

level: juniorimportance: must knowfreq 68%

basics

~20 s

The first render happens with no data, because the request only starts once the component exists. The user waits for that dataless frame plus a full round trip, and every nested component that fetches repeats the pattern.

open as a page

When a component triggers a write to the server, which states does that write pass through, and why disable the trigger while it is pending?

level: juniorimportance: must knowfreq 72%

basics

~20 s

A write passes through idle, pending, then success or failure, and the component renders each. Disabling the trigger while pending stops a duplicate write the server may apply twice, whose two responses can also land in either order.

open as a page

When a component starts a request on every keystroke in a text field, how do debouncing and throttling differ?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Debouncing waits for a quiet gap after the last keystroke and then fires once; throttling fires at a fixed maximum rate while typing continues. A search field wants debouncing, because only the settled query matters.

open as a page

For a client-side cache of server data keyed by the request, what does a stale entry mean and what does a reader get?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Stale means the entry is still usable but no longer trusted to match the server. A reader gets the cached value immediately as data, not as loading, while the cache refetches behind it and re-renders on arrival.

open as a page

How does rendering a pending state from each component's own flags differ from one ancestor fallback covering a subtree that declared itself not ready?

level: middleimportance: must knowfreq 62%

basics

~20 s

Per-component flags give each component its own placeholder: fine granularity, many indicators, coordination by hand. A subtree that declares itself not ready hands one ancestor fallback the whole region: one coherent placeholder, but everything inside waits for the slowest part.

open as a page

When a child component's request needs a value from its parent's response, which part of that waterfall can you remove?

level: middleimportance: must knowfreq 70%

basics

~20 s

Only a request whose input comes from another response has to wait for it. Most nested waits are accidental: the child's key was already available from the URL or a prop, so hoisting the declaration lets both start together.

open as a page

When a push channel delivers data no component requested, where must those events land for the tree to update?

level: middleimportance: must knowfreq 56%

basics

~20 s

In the same cache entries components already read. A push channel is just another writer of existing keys, so views re-render through their normal subscription and no component cares whether a value came from a request or an event.

open as a page

What does an optimistic update write into the cache before the server answers, and what does it need to roll back?

level: middleimportance: must knowfreq 64%

basics

~20 s

An optimistic update patches the cached copy with the client's predicted result before the request settles. Rollback needs the previous value of exactly the entries it touched plus the patch's own identity; success needs reconciliation with the server's representation.

open as a page

Why can an earlier response overwrite newer data when a component restarts its request after its input changes?

level: middleimportance: must knowfreq 78%

basics

~20 s

Responses can come back in a different order than the requests went out, and both write to the same state, so the slower earlier one lands last and wins. A latest-wins rule fixes it: only the newest request may write.

open as a page

In a cache of server data that components subscribe to by key, what stages does an entry pass through before it leaves memory?

level: middleimportance: must knowfreq 58%

basics

~20 s

An entry is created on first request, stays fresh inside its staleness window, goes stale past it, becomes unused when its last subscriber unmounts, and is collected after the retention window. Two independent timers govern it.

open as a page

A pushed update for a key arrives while a request for that same key is still in flight — which write wins?

level: seniorimportance: must knowfreq 52%

basics

~20 s

Not simply the last to arrive: a response reflects server state from when the request started, so it can be older than an event landing after it. Order writes by a server-owned revision, or refetch when none exists.

open as a page

For a list fed by a server request, why do a first-run empty collection and a filtered zero-results view need different branches?

level: middleimportance: should knowfreq 56%

basics

~20 s

The payload is the same empty collection, but the cause and the next step differ. Nothing created yet needs the action that creates the first item; a filter that excluded everything needs widening or clearing. One shared message misleads one audience.

open as a page

What changes when a screen's data is requested at the route boundary before that screen renders, rather than inside its components?

level: middleimportance: should knowfreq 62%

basics

~20 s

The wait moves in front of the screen: a loader on the route starts the screen's requests before the destination renders, so the screen arrives complete and with no internal waterfall. The price is a slower-feeling navigation.

open as a page

How do you decide whether a pushed event should replace a cache entry, patch one field, or invalidate it?

level: middleimportance: should knowfreq 47%

basics

~20 s

By what the payload can support: a full resource replaces the entry, a partial change with a stable identity patches it, a bare notification only invalidates the affected keys. Never patch from a payload that leaves the record inconsistent.

open as a page

After a successful write, when would you patch the cached entry from the response instead of invalidating it and refetching?

level: middleimportance: should knowfreq 56%

basics

~20 s

Patch when the response carries the complete updated form of exactly what changed and nothing derived elsewhere depends on it. Invalidate and refetch when the write moves counts, ordering, pages or other entities, or when its response says nothing useful.

open as a page

How does a shared request layer stop three components that ask for the same resource at once from sending three requests?

level: middleimportance: should knowfreq 58%

basics

~20 s

By keying requests in flight: the first ask starts the request and stores its pending result under a key derived from the request; identical asks arriving while it is pending attach to that same pending result.

open as a page

How does a cache that stores one entry per request differ from one that normalizes entities, and when does the difference matter?

level: middleimportance: should knowfreq 50%

basics

~20 s

A keyed cache stores each response under the request that produced it, so one record can sit in several entries, each refreshed separately. A normalized cache stores each record once by identity, so one update reaches every reader.

open as a page

Why does a client cache usually hold a paginated list as one entry with many pages rather than one entry per page?

level: middleimportance: should knowfreq 38%

basics

~20 s

The accumulated list is what the user sees, so it needs one loading state, one refetch and one invalidation. Pages are not independent either: a cursor page depends on the previous response, and offset pages shift when rows are inserted.

open as a page

When a data request fails, how do you decide between rendering an error branch in the component and letting the failure escalate to an ancestor handler?

level: seniorimportance: should knowfreq 58%

basics

~20 s

Decide by blast radius and by what can be retried. A local branch keeps the rest of the screen alive and a precise retry next to the component that issued the request. Escalate when the failure invalidates the whole region.

open as a page

A dashboard re-requests its data every 30 seconds and every panel blanks back to its loading placeholder on each poll — why, and what fixes it?

level: seniorimportance: should knowfreq 54%

basics

~20 s

Each panel branches on “a request is in flight” instead of “there is nothing to show”. With a previous value held, keep rendering it and mark progress quietly; reserve the placeholder for the load that has nothing to replace.

open as a page

Why does starting a destination's data request at navigation intent change the perceived wait, and what does that cost?

level: seniorimportance: should knowfreq 54%

basics

~20 s

Intent arrives before the click, so the request overlaps the user's decision time and is in flight when the screen renders. The costs: requests for destinations nobody opens, and a result the destination must find by key.

open as a page

After a push channel drops and reconnects, why are the cache entries it fed wrong, and what restores them?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Because the changes made while disconnected were never delivered, so entries sit at pre-gap values with nothing marking them stale. Reconnect must resubscribe and trigger a catch-up read of the keys that still have readers.

open as a page

Several components read the same live key across navigations — how should the push subscription's lifetime be scoped?

level: seniorimportance: should knowfreq 44%

basics

~20 s

To the set of interested readers, not to one component. Reference-count per key in the cache layer: the first reader opens the subscription, later readers join it, and the last teardown closes it after a short grace window.

open as a page

Two writes to the same item are in flight and the row flickers back to an old value; how do you make the settled state deterministic?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Stop treating each response as the final word. Track how many writes to that entity are in flight, derive the view from a base value plus ordered pending patches, ignore or queue superseded writes, and revalidate once nothing is pending.

open as a page

When the component that started a request is destroyed before the response arrives, should that request be cancelled?

level: seniorimportance: should knowfreq 55%

basics

~10 s

It depends who owns the request. A read whose only consumer was that instance should be cancelled; a read a shared layer keeps for other consumers should finish. A write already sent must complete.

open as a page

A cached list refetches on mount, on window focus and on reconnect, and users see constant loading. How would you decide which triggers to keep?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Separate two symptoms: too many requests, and content replaced by a placeholder. Loading over an existing entry points at a missing or unstable key, not a trigger. Then keep each trigger only where data changes unobserved and refetching is cheap.

open as a page

Where should a screen's data request be declared: inside the component that reads it, on the route, or in a shared definition?

level: principalimportance: should knowfreq 44%

basics

~20 s

Separate the request's definition from its start site. Describe it once, parameterised and keyed, so any layer can refer to it; then choose who starts it per surface — the route, an intent handler, or the component that reads it.

open as a page

How do you decide which writes in a product get an optimistic update and which wait for the server?

level: principalimportance: should knowfreq 40%

basics

~20 s

Weigh how predictably the client can compute the result against the cost of being wrong. Optimism fits reversible, self-contained, frequent changes; a write the server decides, or whose reversal would confuse or cost, should wait.

open as a page

Should latest-wins and cancellation rules live in each component that requests data, or in one shared request layer?

level: principalimportance: should knowfreq 44%

basics

~20 s

Put the policy in one shared layer once more than a handful of screens race: request identity, latest-wins, deduplication and cancellation then hold by default rather than per call site. Per-component guards suit only one-off, screen-private requests.

open as a page

showing 1–30 of 33