After a release changes the shape of a route's data payload, why can a tab loaded from the previous build break on its next navigation?
answer
- data from one release, renderer from another
- the payload shape is an implicit contract
- additive changes survive, moves do not
- expand in one release, contract in the next
- a silent wrong render beats no error
basics
~20 sA navigation fetches the route's data but renders it with code from the release that tab loaded. If the new release renamed, removed or re-nested fields, the old renderer reads what is no longer there and blanks or throws.
solid answer
~50 sAfter the first document, a navigation does not fetch a new page; the client router fetches the target route's data and renders it with the components it already has. Those components were compiled against the previous release's data shape. When the new release renames `title` to `heading`, moves a field one level deeper, or drops one entirely, the old code reads `undefined` and either renders a blank or throws while rendering. The reverse also bites: a payload that now omits something the old code treated as guaranteed. The safe discipline is to treat the payload as a contract with an out-of-date consumer — change it additively first, remove the old field a release later, and have the client detect a shape it cannot understand and fall back to a full document load rather than rendering nonsense.
go deeper
Remember that after the first load a navigation brings back data, not new application code, so the components rendering that data can be older than the data itself.
Explain the shape contract: which changes an old renderer absorbs (additions) and which it cannot (renames, removals, re-nesting, type changes), and what each one looks like on screen.
Show the release discipline — expand then contract across two deploys, a detectable payload identity, and error reporting tagged with the client build so you can see the post-deploy cluster.
Judge how long payload compatibility must hold: it is bounded by observed session length, and treating route payloads as permanently public contracts costs delivery speed for no benefit.
## What a client-side navigation actually fetches On the first visit the browser receives a document. After that, most meta-frameworks stop asking for documents. The client router intercepts the navigation, fetches the target route's **data** in a serialised form, loads whatever script chunk that route needs, and renders the result in place. Two separate things therefore travel across a deploy boundary: - **the data** — produced by the new release's server-side loading code; - **the code that renders it** — downloaded when the tab first loaded, and therefore from the previous release. They are compiled against each other, but only within a single build. Nothing checks that the payload a new server produced matches the expectations of an old renderer. (The *format* the payload is serialised in is a separate subject; what matters here is that its **shape** — which fields exist and where — is an implicit contract between two releases.) ## Why the old client cannot cope The old renderer reads fields positionally in its own source: it destructures, it indexes, it maps over a list it believes is there. Every one of those reads is an assumption made by a build that no longer exists on the server. | Change in the new release | What the old client does | Severity | |---|---|---| | Field renamed | Reads `undefined` where a value was required | Blank or broken UI, sometimes a thrown render | | Field removed | Same, plus any derived computation collapses | Usually visible and confusing | | Field re-nested one level deeper | Reads an object where it expected a string | Renders an object placeholder or throws | | Type changed (string to object, scalar to list) | Iterates or formats the wrong thing | Often a thrown render | | New field added | Ignores it | Harmless — this is the safe direction | | List entries gain a required discriminator | Falls into a default branch for every entry | Silently wrong, the worst outcome | The last row is the one to remember in an interview. **A crash is a good failure**: an error boundary catches it and the user reloads. Silently rendering wrong values is worse, because nobody notices that the page came from a stale client. ## Making a payload change survivable The rule is the one used for any contract with a consumer you cannot upgrade in lockstep — and inside a skew window, the old tab *is* exactly that consumer, even though you own its code. 1. **Expand, then contract.** Release one adds the new field and keeps writing the old one. Release two, after the skew window has elapsed, removes the old field. Both halves of the window see a payload they can read. 2. **Add, do not move.** Re-nesting or renaming is a remove plus an add executed at the same instant, which is precisely what an old client cannot absorb. 3. **Read tolerantly.** Rendering code that treats unknown values as optional and has a defined behaviour for a missing one degrades instead of exploding. 4. **Carry a shape identifier.** If the payload names the deployment or the payload revision it came from, the client can compare it to its own and choose a full document load when they differ — turning an unpredictable render failure into a deterministic refresh. 5. **Fail forward, not silently.** When rendering does throw, the boundary that catches it should offer a reload rather than a dead end, because a reload genuinely fixes the underlying cause. ## Where the responsibility sits Meta-frameworks differ here. Some version the payload envelope themselves and force a hard navigation when a client asks for data from a build it was not made for; others hand the response straight to old rendering code and leave the consequences to the application. The mechanism to check on any given stack is: **does a data response from another deployment get detected at all, or is it just parsed?** The practical consequences for a team: - Schema changes to a route payload deserve the same review as a public API change *for the duration of the skew window*, not forever. - "We ship the frontend and the backend together, so contracts do not matter" is true across the deploy and false across the window after it. - The blast radius is bounded: the worst case for a read is a broken view that a reload repairs, which is why teams accept a window at all instead of paying for full deployment pinning. - Instrumentation matters more than defensiveness: tag render errors with the client's build identifier, watch whether they cluster in the minutes after a deploy, and only then decide how much compatibility work each release deserves.
- Why is a renamed field more dangerous than a newly added one?An added field is ignored by old code, so nothing breaks. A rename is a removal and an addition performed together: the old renderer looks for a key that no longer exists and gets nothing back. Adding the new name while still emitting the old one splits that into two safe releases.
- Why can a payload change that throws be preferable to one that renders?A thrown render hits a boundary, which can report the error and offer a reload — the cause is fixed by a full document load. A payload that quietly falls into a default branch shows the user plausible but wrong content, produces no error signal, and can be acted on before anyone notices.
saying these in an interview costs you the question
- Says shipping client and server together removes payload compatibility concerns
- Treats a field rename as equivalent in risk to a field addition
- Assumes a navigation fetches a fresh document and therefore fresh code
- Blames a caching layer for values a stale renderer never read
- Adds defensive checks everywhere instead of sequencing the change across two releases