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.
Understand
Explain why start order does not guarantee completion order.
Build
Ignore a result whose Effect setup has been replaced.
Check
Compare ignoring and cancellation honestly.
Accept only the completion owned by the current selection
Select Maya
Start one local report operation.
Select Ravi
Cleanup marks Maya’s setup obsolete.
Receive results
Only Ravi’s current setup may publish a report.
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.
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.