In a React app, what does it mean that data fetched from an API is stale as soon as it arrives, and which events should make you revalidate it?
answer
- accurate only at serialization time
- how wrong may this get
- your own write is the strongest signal
- focus, reconnect, interval, push
- keep the old value while refreshing
basics
~20 sA response describes the server at the moment it was serialized; every instant after that it is an assumption. Revalidation triggers are a design decision per data type: after your own mutation, on remount, on tab focus or reconnect, on an interval, on user request, or on a server push.
solid answer
~50 sThe moment the response leaves the server it stops being a fact and becomes a snapshot, because anyone else can write to the same row. So for each piece of server data you have to answer two questions: how stale is it allowed to get, and what should trigger a refresh. The triggers worth naming are your own mutations (always — a write invalidates what it touched), a new consumer mounting against a cold or aged key, the tab regaining focus or the network reconnecting after the user has been away, a polling interval for genuinely live values, an explicit refresh control, and a server push over websocket or SSE when the data truly must be live. The tolerance is per data type: an org's name can be an hour old, an account balance cannot. And revalidation should normally be a background refresh that keeps the current value on screen rather than dropping back to a spinner.
go deeper
Be able to say that a fetched value describes the server at one instant only, and that an effect running once on mount means the screen will never notice a later change.
List the revalidation triggers — own mutation, mount, focus, reconnect, interval, explicit refresh, server push — and explain why the staleness tolerance is chosen per data type rather than globally.
Show judgment about cost and disruption: which keys deserve focus refetching, when polling beats a push, why a background refresh must not replace a value the user is interacting with, and how you make freshness observable.
Frame freshness as a product-level contract per data domain, with defaults, exceptions and a way to see when they are violated. Weigh the operational cost of push infrastructure against polling volume and the business cost of showing a wrong number.
## "Stale on arrival" is literal A JSON response describes the server's state at the instant it was serialized. By the time it has crossed the network, been parsed and rendered, another user, a background job or your own second tab may already have changed the underlying row. Nothing in the response tells you that. So the honest model is: **a fetched value is a snapshot with an unknown and growing error**, not a fact. This is not a reason for anxiety; it is a reason to make two decisions explicitly instead of by accident. ## Decision one: how stale may this be? Staleness tolerance is a property of the *data*, not of the app, and it varies by orders of magnitude on a single screen: - A company name or a country list: hours or the whole session. Refetching it is waste. - A list of the user's documents: seconds to a minute. Wrong-but-recent is tolerable; wrong-after-you-just-added-one is not. - An account balance, a seat-availability count, a live auction price: near-zero. Here you either push updates or you refuse to render a number you cannot vouch for. The interview-grade answer says the tolerance out loud per data type rather than applying one global policy. A single "refetch everything every 30 seconds" rule is both too aggressive for reference data and far too lax for money. ## Decision two: what triggers a refresh? There are only a handful of real triggers, and naming them in order is a good answer: **Your own mutation.** The strongest signal by far — you *know* the data changed because you changed it. Every write should either write the server's response into the cached entry or mark the affected keys stale. This is the trigger teams most often forget, and it produces the classic "I edited it and the list still shows the old value" report. **A consumer mounting.** When a component starts reading a key, revalidate if the entry is older than the tolerance you chose. If it is fresh, render it immediately with no request at all. **Focus and visibility.** The user tabbed away for ten minutes and came back. Anything they are now looking at is likely aged. Refetching on focus or on `visibilitychange` is a cheap way to make an app feel current, but it is not free — it is wrong for a half-filled form and wrong for data that never changes. **Reconnect.** The device was offline; whatever happened during that window was missed entirely. Revalidating on `online` closes that gap. **An interval.** Polling is the blunt instrument. It is correct for dashboards and queues where the data genuinely moves on its own and precision is not required. Make the interval a property of the data and pause it when the tab is hidden. **An explicit user action.** A refresh control is not a cop-out; for expensive or high-volume data it is often the right answer, and it makes the freshness contract visible to the user. **A server push.** Websocket or SSE messages invert the model: the server tells you a key changed and you invalidate or patch it. This is what you reach for when the tolerance is near zero, and it is a real operational commitment, not a free upgrade. ## Revalidate in the background, not by unmounting the data The common implementation mistake is to treat a revalidation like a first load: clear the value, show a spinner, then show the new data. That makes the app feel worse every time it tries to be more correct. Keep the current value on screen, fetch alongside it, and swap when the response lands — optionally with a subtle pending indicator. The distinction to be able to state is between **no data yet** (a real loading state, show a skeleton) and **data that may be old** (show it, refresh underneath). ## Where React itself sits in this React gives you no staleness model at all. `useState` will hold a value from last Tuesday just as happily as one from a second ago, and an effect with an empty dependency array is a policy statement — "fetch once, never again" — that most people write without realizing they made it. Everything above is a policy you either implement or inherit from a cache layer. What React does contribute is the ability to make a background refresh non-disruptive: updating cached data inside `startTransition` lets the current UI stay interactive while the new result renders. ## The sentence that summarizes it *Freshness is a product decision expressed in code.* If nobody chose the tolerance and the triggers for a given piece of data, the app still has a policy — it is just whatever the empty dependency array happened to imply.
- Why is refetching on window focus sometimes the wrong default?Because focus is a proxy for "time has passed", not evidence that anything changed. It wastes requests on data that never moves, it can fire a burst across many keys when a user alt-tabs repeatedly, and it can disrupt a half-completed interaction — a form seeded from server data, or a list the user is mid-scroll through — if the refresh is allowed to replace what is on screen.
- How do you decide between polling and a websocket push for near-live data?Polling is cheap to build and degrades predictably, so it wins when the tolerance is seconds and the number of clients is modest. A push wins when the tolerance is sub-second, when updates are rare but must be immediate, or when polling cost scales badly with users. A push is also an operational commitment: reconnection, missed-message recovery and auth all become yours.
- What is the difference between showing a spinner and showing stale data during a revalidation?A spinner is honest when there is nothing to show — a cold key on first load. Once you have a value, replacing it with a spinner destroys information the user already had and makes correctness feel like a regression. Render the existing value, fetch underneath, and swap on arrival, with at most a subdued pending indicator.
saying these in an interview costs you the question
- Assumes fetched data stays correct until the page reloads
- Applies one global refetch interval to every kind of data
- Clears the value and shows a spinner on every revalidation
- Forgets that a successful write invalidates what it changed
- Treats an empty dependency array as if it were not a policy