The Problem with Loading Spinners
In traditional web development, the user interaction loop is highly synchronous: the user clicks a button, a loading spinner appears, the browser makes an HTTP request to the API, and only when a successful response returns does the UI update. For a heavy form submission, a loading state is necessary. But for micro-interactions—like starring a repository, checking off a task, or upvoting a comment—making the user wait 300 milliseconds for a server response makes your application feel sluggish and outdated.
In modern B2B SaaS platforms at Smart Tech Devs, we architect for perceived performance. The solution is Optimistic UI Updates.
The Optimistic UI Paradigm
Optimistic UI flips the script. When the user takes an action, we immediately update the frontend state as if the API request has already succeeded. We give the user instant visual feedback. In the background, the actual API request is firing. If the request succeeds, great. If the request fails, we silently roll back the UI to its previous state and show an error notification.
Managing this rollback logic manually in React is a nightmare of useState and complex error handling. However, using a robust async state manager like TanStack Query (formerly React Query) makes optimistic updates incredibly elegant.
Implementing Optimistic Updates
Let's look at how we architect an optimistic update for toggling a specific task's completion status in a project management dashboard.
// components/TaskItem.tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
export default function TaskItem({ task }) {
const queryClient = useQueryClient();
const mutation = useMutation({
// 1. The actual API call
mutationFn: (newStatus: boolean) => {
return axios.patch(`/api/tasks/${task.id}`, { completed: newStatus });
},
// 2. The Optimistic Update (Runs immediately when mutate() is called)
onMutate: async (newStatus) => {
// Cancel any outgoing refetches so they don't overwrite our optimistic update
await queryClient.cancelQueries({ queryKey: ['tasks'] });
// Snapshot the previous value for a potential rollback
const previousTasks = queryClient.getQueryData(['tasks']);
// Optimistically update the cache instantly
queryClient.setQueryData(['tasks'], (oldData: any) => {
return oldData.map((t: any) =>
t.id === task.id ? { ...t, completed: newStatus } : t
);
});
// Return a context object containing the snapshotted value
return { previousTasks };
},
// 3. If the mutation fails, use the context to roll back
onError: (err, newStatus, context) => {
queryClient.setQueryData(['tasks'], context?.previousTasks);
alert("Failed to update task. Please try again.");
},
// 4. Always refetch after error or success to ensure 100% server sync
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
});
return (
<div className="flex items-center gap-3">
<input
type="checkbox"
checked={task.completed}
onChange={(e) => mutation.mutate(e.target.checked)}
/>
<span className={task.completed ? "line-through" : ""}>
{task.title}
</span>
</div>
);
}
The Impact on Perceived Performance
By implementing this pattern, your application instantly feels like a native desktop app. The latency between the client and your Laravel API is completely masked from the user. They can rapidly click, toggle, and interact with your dashboard without ever being interrupted by a blocking loading state.
Conclusion
User experience is dictated by perceived speed, not just actual server response times. By leveraging TanStack Query to architect robust Optimistic UI updates, you eliminate the friction of micro-interactions and build fluid, highly responsive SaaS platforms that users love to interact with.