Spinners explain almost nothing
A spinner tells users the interface is busy. It does not tell them whether the request reached the server, whether anything was saved, or whether it is safe to leave the page.
For a public article, a small skeleton may be enough. For a form that uploads a resume or changes an account setting, the waiting state carries a much bigger job: prevent duplicate action and preserve confidence.
Name the phase the user is in
Start with the actual lifecycle of the operation:
idle -> validating -> submitting -> confirmed
-> failed
-> outcome unknown"Outcome unknown" deserves its own place. A network timeout does not prove the server rejected the write. If a request may have succeeded, a retry could duplicate it unless the operation is idempotent.
The UI copy can reflect these distinctions:
- "Checking your details" when validating.
- "Submitting your application" while the write is in flight.
- "Application received" only after confirmation.
- "We could not confirm the result. Check your applications before retrying" when the response is lost.
Empty is not loading
An empty list after a successful fetch is a real state, not a failure or endless loading placeholder. Tell the user what it means and what they can do next.
Loading: We're checking for open roles.
Empty: There are no open roles right now. General applications are open.
Error: Roles could not be loaded. Try again.Each message describes a different reality. Reusing one blank screen for all three saves code at the cost of user trust.
Show progress at the right boundary
React can keep an interface responsive while a transition is pending, and Next.js can stream server-rendered content behind a loading boundary. Those tools are useful, but the copy and placement should match the work being done. A loading indicator for one slow sidebar should not make the entire page look unusable.
For writes, keep the submitted input visible until the result is known. Preserve the user's draft on validation or network failure. Disable only the control that would duplicate the action, and give the user a clear retry path once the outcome is known.
Measure the moments between states
When testing a critical flow, deliberately add latency and failure. Watch for these questions:
- Can the user tell that their action was registered?
- Can they distinguish waiting from success?
- Can they recover without re-entering data?
- Could a retry cause the action twice?
The best loading state is not the prettiest animation. It is the smallest honest explanation of what the system knows right now.