The invisible work behind a Vue or React SPA
Whether a project uses Vue or React, I have found it easy to think of a single-page application as a set of routes and screens. A route renders, a component appears, and the work can seem finished.
Over time, I have come to see the page as only the visible part. Much of the user experience lives in the moments around it: while data is loading, when an action is pending, when a request fails, or when there is simply nothing to show.
A screen is more than its default state
The default state is usually the easiest one to build. It has the expected data, the expected layout, and a user who follows the happy path.
Real use is often less tidy. Data can take time to arrive. A list can be empty. A form can fail to submit. Content can be longer than the sample in a design. A user can return to a page after changing a filter or pressing the browser’s back button.
None of these cases are unusual, but they easily become afterthoughts in a Vue or React SPA. Thinking about them early has helped me move from asking “does this page render?” to asking “does this experience still make sense while the page changes?”
- Default
- Loading
- Empty
- Error
- Pending
Most of the experience lives between states
A loading state tells users that the product is responding. An empty state explains what is missing and, when useful, what to do next. An error state should help someone recover rather than leave them at a dead end. A pending action should make it clear that the product has received the input, even before the final result arrives.
These states do not need to be elaborate. Often a short message, a sensible placeholder or a disabled action is enough. What I try to avoid is leaving users guessing whether the product is working, or whether their action had any effect. It is the first of Nielsen’s usability heuristics for a reason: visibility of system status.
This is one place where a shared component and a few clear UI guidelines can help. The same loading indicator or error pattern will not solve every problem, but it gives the product a familiar way to say that something is still in progress.
Give each kind of state a home
One approach that has helped me is being deliberate about where state belongs. A search query, selected filter, sort order or page number often fits naturally in the URL, because it survives a refresh, can be shared and works with browser navigation. Data that comes from an API usually benefits from a clear loading, error and refresh lifecycle. Temporary UI details, such as whether a dropdown or modal is open, can stay close to the component that owns them.
- URLSearch, filters, sort, pageSurvives a refresh, can be shared, works with the back button.
- Server dataWhat comes from the APINeeds a clear loading, error and refresh lifecycle.
- ComponentOpen dropdowns, modalsTemporary UI that can stay close to the component that owns it.
// A search screen whose state lives in the URL
/products?q=lamp&sort=price&page=2
const params = new URLSearchParams(location.search);
params.get("sort"); // "price"Vue and React offer different tools and patterns for this (the React docs on choosing the state structure are a good example), but the underlying question is similar: if this value changes, who needs to know, and what should happen next?
When that answer is unclear, state spreads across the application. A screen may still work, but it becomes harder to explain, test and change without affecting something else.
Help people stay oriented
Users may not care whether a product is a Vue SPA or a React SPA. They are more likely to notice whether they lose their place, whether a click has a clear result, and whether the product gives them enough context to continue.
For me, that is the invisible work behind a good SPA. Routes and components make the application possible. Careful handling of state and feedback is what makes it feel dependable while people use it.
Working on something similar? Let's talk