Enhancing User Experience
Practical tips for improving the user experience during data fetching in React apps.
Perceived performance, in the end, is the only performance that truly matters. If what we make doesn't feel fast, then no amount of optimization counts.
Understanding App Performance
When we talk about performance, there are two types: actual performance and perceived performance.
- Actual performance is what you can measure - numbers from tools like Lighthouse.
- Perceived performance is how fast your app feels to users.
Your app might score 98 on Lighthouse, but if users still think it’s slow, that number doesn’t mean much.
Here are some simple ways to make your web app feel faster, even if it’s not truly faster.
Visibility of System Status
Never leave users guessing. Always show feedback when something is happening, like a loading bar for uploads, a spinner for data fetching, or a button state change for form submissions. This tells users the app is working, keeps them informed, and prevents frustrated repeat clicks. The feedback should appear within 1 second for any action that takes time to complete.
In React Query, there are three loading states (isLoading
, isFetching
, isPending
), the best practice is to handle them differently:
1. isLoading
isLoading
becomes true
when the query runs for the very first time, like after the app initially mounts or on a full page reload.
This is the ideal moment to display a global <Spinner/>
or <Skeleton/>
to communicate to users that the main content is loading for the first time.
2. isFetching
isFetching
is true
any time a query is actively refreshing in the background,
such as after changing filters, pagination, or refetching.
Use this state to show lightweight progress indicators (NProgress
)
while keeping existing data visible, ensuring the UI doesn't flicker or shift.
Example:
Additionally, when fetching lists, keep in mind:
- Set
placeholderData: keepPreviousData
in the query options to avoid layout shift. - Because list screens often include search and filters, the
queryKey
changes frequently, which can trigger many requests. Pass the abortsignal
so the query can cancel unnecessary requests.
Want to see what the isFetching looks like? Take a look here.
3. isPending
isPending
is true
when a mutation is running.
Use this to provide UI feedback during user-triggered actions,
such as disabling submit buttons or showing a loading spinner, so users know something is happening.
- If the action is a single button, disable that button, for example:
- For form submissions, wrap the entire form with
LoadingOverlay
, for example:
Use Optimistic UI
Instead of waiting for the server response after a button click, immediately reflect the change in the UI—then update the server in the background. This lets your app feel more responsive, even if the backend is slow.
But keep in mind: only use optimistic updates for actions that almost always succeed, otherwise, failed actions could make the experience worse for users.
Tanstack Query offers a clear guide on implementing optimistic UI. Our main recommendation is to use the "via the UI" approach, as it's usually simpler than other methods.
For optimistic UI example (deleting an item), check out this demo
Additional techniques
Here are some additional, less critical techniques you don't strictly need, but they're good to be aware of:
- Prefetching data in advance - source
- Running multiple queries in parallel - source
- Preventing request waterfalls - source
Final Thought: Fast Is a Feeling
Performance isn’t just about numbers, it’s about how your app makes users feel. People won’t remember that your page loaded in 1.2 seconds, but they’ll remember that it felt easy and smooth to use.