ProgramGuru

Preventing Stale Responses: Keep Results with Their Selection

Preventing Stale Responses: Keep Results with Their Selection

A teacher requests Maya’s report and then selects Ravi. If Maya’s slower response finishes last, it must not overwrite the currently selected Ravi report.

A stale response belongs to an older request or selection. Prevent it from updating the current interface by checking ownership when it finishes. Cancelling supported work can reduce waste, but ownership checks still express which result is relevant.

  1. Understand

    Explain why start order does not guarantee completion order.

  2. Build

    Ignore a result whose Effect setup has been replaced.

  3. Check

    Compare ignoring and cancellation honestly.

Accept only the completion owned by the current selection

  1. Select Maya

    Start one local report operation.

  2. Select Ravi

    Cleanup marks Maya’s setup obsolete.

  3. Receive results

    Only Ravi’s current setup may publish a report.

Read this relationship alongside the complete code. This diagram explains the flow; it does not execute React.

Capture ownership for each Effect setup

Each render has its own bindings. An Effect for Maya creates one ignore variable, and the Effect for Ravi creates another. Cleanup changes the old setup’s variable so its promise handler knows it no longer owns the screen.

The example deliberately uses local timers with different delays to model out-of-order completion. It does not perform a network request and does not claim a real server’s response speed. The essential property is checking relevance when an operation finishes, not assuming a particular delay.

Ignore a slower local report after changing students

Maya’s local report waits longer than Ravi’s. Click Choose Ravi while Maya is pending. The screen should eventually show Report for Ravi and remain there even when Maya’s earlier timer completes. The timers simulate latency; the React ownership mechanism is real.

React · JavaScript with JSX

import { useEffect, useState } from 'react';
function localReport(name) {
  return new Promise(resolve => setTimeout(() => resolve('Report for ' + name), name === 'Maya' ? 80 : 10));
}
export default function App() {
  const [student, setStudent] = useState('Maya');
  const [report, setReport] = useState('Loading report');
  useEffect(() => {
    let ignore = false;
    setReport('Loading report');
    localReport(student).then(text => { if (!ignore) setReport(text); });
    return () => { ignore = true; };
  }, [student]);
  return <main><h1>Current report</h1><p>Selected: {student}</p><p role="status">{report}</p>
    <button onClick={() => setStudent('Ravi')}>Choose Ravi</button></main>;
}

Expected screen and behavior

Choose Ravi before Maya finishes. Ravi’s report displays, and the older Maya result is ignored.

Follow the two independent ignore bindings

React · Ignore a slower local report after changing students

import { useEffect, useState } from 'react';
function localReport(name) {
  return new Promise(resolve => setTimeout(() => resolve('Report for ' + name), name === 'Maya' ? 80 : 10));
}
export default function App() {
  const [student, setStudent] = useState('Maya');
  const [report, setReport] = useState('Loading report');
  useEffect(() => {
    let ignore = false;
    setReport('Loading report');
    localReport(student).then(text => { if (!ignore) setReport(text); });
    return () => { ignore = true; };
  }, [student]);
  return <main><h1>Current report</h1><p>Selected: {student}</p><p role="status">{report}</p>
    <button onClick={() => setStudent('Ravi')}>Choose Ravi</button></main>;
}

The Maya setup starts with ignore false. Selecting Ravi causes React to clean up Maya’s Effect before setting up Ravi’s. The old ignore becomes true; the new setup has its own false flag.

When Ravi completes, its flag permits setReport. When Maya completes later, its old flag prevents setReport. There is no global ignore variable that accidentally disables all future requests. This handles displayed ownership even though the local promise itself is not cancelled.

Choose cancellation and presentation separately

For actual fetch, use AbortController if appropriate and handle abort-related rejection. The ignore guard can still make the relevance rule clear. For libraries that supply request keys and cancellation, follow that library’s documented ownership model rather than layering unrelated global flags.

A related issue is briefly displaying a previous report under a newly selected heading before a resetting Effect runs. Production interfaces can store results with the student ID and only display a result when its owner matches the current ID, or key the detail component by ID. This simple example focuses on late completion; refine presentation as requirements demand.

Try the school task

React · starting example

import { useEffect, useState } from 'react';
function localReport(name) {
  return new Promise(resolve => setTimeout(() => resolve('Report for ' + name), name === 'Maya' ? 80 : 10));
}
export default function App() {
  const [student, setStudent] = useState('Maya');
  const [report, setReport] = useState('Loading report');
  useEffect(() => {
    let ignore = false;
    setReport('Loading report');
    localReport(student).then(text => { if (!ignore) setReport(text); });
    return () => { ignore = true; };
  }, [student]);
  return <main><h1>Current report</h1><p>Selected: {student}</p><p role="status">{report}</p>
    <button onClick={() => setStudent('Ravi')}>Choose Ravi</button></main>;
}

Add a class selection with local reports. Accept only the latest selected class’s completion, even if an earlier operation is slower.

Worked answer

The class dependency gives each selection an Effect setup. Cleanup invalidates that setup, and its handler checks before publishing. The complete source makes the protection visible rather than relying on an earlier section’s helper.

React · JavaScript with JSX

import { useEffect, useState } from 'react';
function loadClass(name) {
  return new Promise(resolve => setTimeout(() => resolve('Ready: ' + name), name === '7A' ? 80 : 10));
}
export default function App() {
  const [className, setClassName] = useState('7A');
  const [message, setMessage] = useState('Loading class');
  useEffect(() => {
    let ignore = false;
    setMessage('Loading class');
    loadClass(className).then(value => { if (!ignore) setMessage(value); });
    return () => { ignore = true; };
  }, [className]);
  return <main><h1>Class report</h1><p>Selected: {className}</p><p>{message}</p>
    <button onClick={() => setClassName('7B')}>Choose 7B</button></main>;
}

Expected screen and behavior

Choose 7B while loading. Ready: 7B remains the current result after the old 7A operation completes.

Mistakes and boundaries

A timer delay is not an exact finish-time guarantee. The ownership check remains useful even when completion order changes or the environment runs slowly.

Ignoring a result does not undo a server mutation, cancel a timer or restore overwritten data. It controls whether this result is allowed to affect the current screen.

Failure must follow the same ownership rule as success. An old request error should not replace the current request’s successful result with an unrelated error message.

Check your reasoning

React · Ignore a slower local report after changing students

import { useEffect, useState } from 'react';
function localReport(name) {
  return new Promise(resolve => setTimeout(() => resolve('Report for ' + name), name === 'Maya' ? 80 : 10));
}
export default function App() {
  const [student, setStudent] = useState('Maya');
  const [report, setReport] = useState('Loading report');
  useEffect(() => {
    let ignore = false;
    setReport('Loading report');
    localReport(student).then(text => { if (!ignore) setReport(text); });
    return () => { ignore = true; };
  }, [student]);
  return <main><h1>Current report</h1><p>Selected: {student}</p><p role="status">{report}</p>
    <button onClick={() => setStudent('Ravi')}>Choose Ravi</button></main>;
}

Choose the answer that follows the data and the interaction, then read the explanation for each alternative.

What does the ignore flag actually prevent?
Choose one answer. You can try again.

Answer and explanation

An obsolete setup’s completion from updating the current report.

Correct. The handler checks ownership before publishing state.

Carry the idea forward

Check relevance at completion rather than trusting start order. Review setup ownership and success and failure paths.

Read the primary reference: React: ignoring obsolete fetch results.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Custom Hooks: Reuse Logic Without Sharing State Accidentally →