Go back
ARTICLE / READING FRAME

React loading and error states: keep the user’s task intact

An interface is not complete merely because its successful response looks good. Design a small interaction that preserves input, explains what is pending and offers a usable route out of failure.

Listen to this articleUsing local device voices

Checking local voices…

React loading and error states: keep the user’s task intact
TL;DR / 01

In brief

An interface is not complete merely because its successful response looks good. Design a small interaction that preserves input, explains what is pending and offers a usable route out of failure.

READING / 02
On this page

Imagine a search box. A user types a word and a request begins, then immediately changes the query. If the first request finishes last, which result should remain visible? That question shows why loading behavior is more than a spinner. Input, request identity, visible results and recovery need to be designed together. This is an interface example, not a measured performance improvement claim.

Define what the user is waiting for

“Loading” may not explain enough. Is the user waiting for a new result list, a file or a saved record? The position and wording of the indicator should answer that question. While someone types a search, consider the affected result region rather than disabling the entire page. An action that creates duplicate records may need an additional submission guard. Do not apply one locking rule to every pending state.

Keeping the previous result can be useful, but presenting it as the new query’s answer is misleading. Explain when results are refreshing and make the older data’s status understandable. Reserving space can reduce large layout shifts. A skeleton should resemble the eventual content area rather than function only as animated decoration. Respecting reduced-motion preferences is part of this design.

Separate the mechanisms

Suspense can show a nearby fallback while a supported resource is pending. Adding an arbitrary fetch inside a component does not automatically connect that request to a Suspense boundary. Consult the framework’s data-loading approach and the behavior of the component you use. Start with a small example and observe when each boundary appears before adding more boundaries to an unexplained loading problem.

A transition does not make the network faster. It relates to how certain updates are prioritized and how existing interface content is preserved. The text field should respond immediately while a heavier result change can be handled separately. Check which updates remain within a transition after asynchronous work. Using an API name is not evidence that every desired behavior now happens automatically.

◈ EXPERIMENT LAB / RACELOCAL ONLY ↗

Can a stale response replace the current result?

A safe simulation. No real requests, messages or payments; choices stay on this page.

A → 1200 msB → 200 ms
Final displayed response: BModel: A then B are pending concurrently; B represents current intent. Identity checking rejects stale A. This is not a real timer.

Catch stale responses with a small test

Use two queries, first “book” and then “bookcase.” Let the second result arrive first. The late first response must not replace the current list. Cancellation, an active request identifier or a data-layer mechanism can help; state which approach is responsible. Also check whether an intentionally canceled request produces an unnecessary error message. Cancellation should not always be presented as a user-facing failure.

You can model delayed responses without burdening a real service. Write the expected screen for normal order, reversed order, empty results and failure. Confirm that each response updates only the query it belongs to. Then test leaving the page. Handling pending work when the user navigates away is part of lifecycle design. The experiment here calculates a simplified race; it does not send requests or reproduce a complete React scheduler.

Offer a way forward after failure

For validation errors, identify the field the user can fix. For network failure, retain the input and offer a suitable retry. An Error Boundary for rendering failures does not replace these behaviors. Review accessible status messages, focus and error wording together. Instead of exposing a technical stack trace, explain what did not complete and the safe next step available.

Do not finish the acceptance pass with a mouse alone. Search with the keyboard, reach the new result, read the error and retry. At increased zoom, check that the message remains associated with its control. On mobile, make sure the recovery action remains visible. A checklist organizes the review but does not certify accessibility. Verify these states in the running interface and record the limits of each check.

Build a state table

Record user action, active request, visible result and available control. Start with no query, then pending search, empty result, valid result and network failure. The meaning of each button should be clear in every row. If a button retries in one state but creates a new record in another, its label and behavior must communicate that distinction. Similar appearance should not hide different external effects.

Follow the table using only the keyboard. Enter a query, wait, revise it after an empty result and retry after a failure. Record unexpected focus jumps or detached error messages as design findings. Imagine the same journey without animation: can the user still understand what is happening? If not, improve the relationship between text, state and controls rather than relying on motion alone.

Sources

Frequently asked questions

Does Suspense catch every fetch request?

No. It works with supported data sources and mechanisms such as lazy-loaded components. A request started in an ordinary Effect is not automatically managed by Suspense. Check the behavior of your framework and data layer.

Does useTransition make the network faster?

No. It helps govern how certain updates are handled in the interface. Wrapping asynchronous code in a transition does not automatically solve request ordering or error recovery. Measure network behavior separately from rendering priority.

Should a controlled text input update in a transition?

Keep the typed value responsive and update it immediately. Treat heavier result changes separately, following React’s documented constraints. Preserving typing behavior matters more than simply adding a loading label.

Does an Error Boundary catch every failure?

No. It handles certain rendering failures in descendant components, not every event-handler or asynchronous failure. Model request errors, validation feedback and retry behavior explicitly alongside the rendering boundary.

What should Retry preserve?

Preserve input, the identity of the failed operation and, where useful, the previous valid result. For an external mutation, assess the effects of repeating it. Refreshing a result list and sending a second message are different operations.