Treat query identity as state
When starting a request, record a generation or request identity alongside the query. A completion may update visible results only when its identity still matches the current mounted view. This rule handles both a late old success and a late old failure: neither belongs to the newer query.
React's useEffect guidance shows a cleanup flag to ignore out-of-order fetch completions. The important contract is not the variable name; it is that cleanup or a generation comparison stops obsolete work from setting current state.
Cancel obsolete work when the API supports it
Ignoring a response protects the interface. Cancellation can additionally avoid work when the request API supports it. The browser's AbortController.abort() can abort a fetch and related asynchronous work. Treat an expected abort as a transition out of pending state, not as an alarming user-facing failure.
Cancellation does not authorize access, guarantee that a server stopped work, or provide server-side ordering. The backend must still enforce identity, permissions, rate limits, and correct query semantics.
Keep visible states distinct
For the current query, distinguish loading, ready, empty, failed, idle, and stale. Do not show an old failure for a newer search term. Do not call an empty current result a network failure. If older rows remain visible while refreshing or after a newer request aborts, label them as stale and retain their query generation rather than silently presenting them as the new query's answer.
Announce the current result without moving focus
Keep focus in the search input while its query settles. Pair the visible result
heading with a concise programmatic status message, such as Loading results for “invoice”, 3 results for “invoice”, No results for “invoice”, or Showing older results while “invoice” reloads. Use a stable container with
role="status" for ordinary loading, result-count, empty, and stale updates;
put an actionable failure in visible text with a keyboard-reachable Retry button.
W3C's ARIA22 status technique
documents role="status" as a polite live-region pattern for application and
user status. Do not use a live region to hide an important error or move focus
unexpectedly. The user must still be able to tab from the input through results
and Retry in a predictable order.
This is an illustrative accessibility contract, not a browser or screen-reader interoperability result. Verify the actual component with keyboard and the assistive-technology combinations the application supports.
Test the ordering rule outside JSX
The local evidence/P92/search-state.mjs model makes the current-generation rule inspectable. It tests older success, older failure, current failure, empty results, abort, retained stale results, and unmount behavior:
npm test --prefix sites/reactjsx.com/evidence/P92
The model is synchronous and framework-independent. The JSX adapter and real-network behavior still need application-level browser coverage.
An illustrative Effect
useEffect(() => {
const controller = new AbortController();
const generation = nextGeneration();
search(query, { signal: controller.signal })
.then(results => acceptIfCurrent(generation, results))
.catch(error => { if (error.name !== "AbortError") failIfCurrent(generation, error); });
return () => controller.abort();
}, [query]);
This is illustrative. The state model establishes only the current-generation decision; it does not browser-test this component or choose a cache library.
Verification checklist
- Every request carries the visible query's identity or generation.
- A completion updates state only when it is still current and mounted.
- Obsolete requests are cancelled where the API supports cancellation.
- An expected abort does not replace the current result with an error.
- Loading, empty, failure, and retained older data are visually distinct.
- Status updates use bounded text; normal updates are programmatically exposed without moving focus.
- Retry remains keyboard reachable and a stale-result label identifies the older query.
- Server authorization and query semantics remain trusted-server concerns.
Frequently asked questions
Is aborting a fetch enough to prevent stale results?
No. Keep the current-identity guard. A completion can be observed at awkward times, and not every request mechanism supports the same cancellation behavior.
Should old results disappear while the new query loads?
That is a product decision. If retained, clearly label them as loading or stale so users do not act on them as if they answered the newer query.