Typorithm · hard

5. Request state and stale-response protection

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

5. Request state and stale-response protection

0%

Concept and contract

Keep UI states valid and prevent an older response from replacing newer results.

A request state should describe valid alternatives: idle, loading, ready with data, and failed with an error. A discriminated union ties each payload to the state that justifies it. Separate booleans can represent contradictions such as successful data and an unrelated active failure.

Correct payload types do not solve response ordering. Two valid requests can complete in the opposite order from their start. Associate each request with a generation or identity and accept a completion only when it still belongs to the current request.

Mechanism and worked example

The example’s complete function ignores a result whose request id differs from the loading state.

The example’s complete function ignores a result whose request id differs from the loading state. A matching result transitions to ready. This is a small pure state transition that can be tested without React, a network or timing-dependent sleeps.

Cancellation can reduce wasted work, but a stale-response guard remains useful because abort can arrive too late or be unsupported. Decide whether previous successful data remains visible during loading; if so, model that retained data explicitly rather than accessing a ready-only field in every state.

Complete worked example

typescript

type State = {kind:"idle"} | {kind:"loading";request:number} | {kind:"ready";items:string[]} | {kind:"failed";message:string};
function complete(state: State, request: number, items: string[]): State {
  return state.kind === "loading" && state.request === request ? {kind:"ready",items} : state;
}
Reasoning
The request id checks relevance after the state tag establishes that a request is loading.

Counterexamples and common mistakes

The rejected read tries to access items on any State.

The rejected read tries to access items on any State. Idle and failed do not have it. A fallback cast would hide the missing branch and potentially show stale or unrelated results. Render each state intentionally, including an explicit empty ready collection.

Do not increment a local variable on every render and assume it is a stable request identity. In React, a ref or effect-local cleanup policy can coordinate asynchronous work. Keep the identity’s lifecycle tied to the actual request rather than incidental rendering.

Rejected counterexample: expected compiler diagnostic

typescript

type State = {kind:"idle"} | {kind:"ready";items:string[]};
function unsafe(state: State): number {
  // @ts-expect-error Idle state has no items.
  return state.items.length;
}
Reasoning
The union requires a branch before accessing state-specific payloads.

Independent practice

Implement fail for the same state machine.

Implement fail for the same state machine. Accept an error only when the request id matches the current loading state. Preserve the original object for stale failures so consumers can avoid an unnecessary state change.

Test matching failure, stale failure and failure while already ready. Optional extension: retain previous ready data during a refresh with a richer state model. Complete the basic pure transition first; asynchronous orchestration can then call it safely.

Step 1: Check loading kind first. Step 2: Accept failure only for the current request id.

Practice starter: solve before reading the solution

typescript

type State = {kind:"idle"} | {kind:"loading";request:number} | {kind:"ready";items:string[]} | {kind:"failed";message:string};
function fail(state: State, request: number, message: string): State { throw new Error("TODO"); }
Reasoning
Implement one pure failure transition before adding network orchestration.
  • Hint 1: Return the original state for stale events.
  • Hint 2: Correct payload types do not establish result relevance.

Solution, tests and trade-offs

The solution checks both kind and request id before constructing a failed state.

The solution checks both kind and request id before constructing a failed state. It preserves identity for ignored events and creates a new record for an accepted transition. This makes the stale policy explicit and easy to test.

Use deterministic event order: start request two, then deliver completion or failure from request one. Assert the newer state remains unchanged. Such a test is stronger than hoping a manual slow-network demonstration happens to produce the race.

Practice solution and acceptance checks

typescript

type State = {kind:"idle"} | {kind:"loading";request:number} | {kind:"ready";items:string[]} | {kind:"failed";message:string};
function fail(state: State, request: number, message: string): State {
  return state.kind === "loading" && state.request === request ? {kind:"failed",message} : state;
}

// Acceptance checks
const state:State={kind:"loading",request:2};if(fail(state,1,"old")!==state||fail(state,2,"new").kind!=="failed")throw new Error("failure");const ready:State={kind:"ready",items:[]};if(fail(ready,2,"late")!==ready)throw new Error("late");
Reasoning
Ignored events preserve state identity; matching events produce the documented transition.

Interview reasoning and next step

In interviews, explain the difference between valid data and currently relevant data.

In interviews, explain the difference between valid data and currently relevant data. Show a concrete out-of-order sequence, then identify which invariant the generation check protects. Discuss cancellation as resource control rather than guaranteed rollback.

Next, connect these state contracts to React props and hooks. Keeping the reducer logic pure makes the framework layer smaller and gives beginners a manageable step between type syntax and a complete asynchronous component.

Optional stretch — after the core task works: Retain previous ready data during a refresh.

Keep UI states valid and prevent an older response from replacing newer results.