I've distinguished between managing the loading state (useTransition) and faking the result (useOptimistic).
The Dynamic Duo: Managing Process vs. Managing Perception
To achieve the "illusion" of speed mentioned in your image, you need two distinct tools:
-
useTransition: Handles the background work. It stops the browser from freezing and tells you if work is happening (isPending). -
useOptimistic: Handles the immediate visual feedback. It updates the UI instantly, assuming the background work will succeed.
The Scenario: A "Like" Button
Imagine a user clicks a "Like" button on a post.
- Without Optimistic UI: The user clicks -> waits 1 second -> the number goes up. (Feels sluggish).
- With Optimistic UI: The user clicks -> the number goes up instantly -> the server catches up in the background. (Feels snappy).
1. The "Standard" Approach (useTransition only)
Here, we rely on the server to tell us the new like count. We use useTransition only to show a loading spinner, so the user knows something is happening.
The Experience: User clicks → Spinner appears → Spinner vanishes & Number updates.
'use client';
import { useTransition } from 'react';
import { incrementLike } from './actions'; // Server Action
export default function StandardLikeButton({ likeCount }) {
const [isPending, startTransition] = useTransition();
const handleClick = () => {
startTransition(async () => {
// 1. We wait for the server to finish,
await incrementLike();
// 2. The UI will update only after the server responds and re-renders the page
});
};
return (
<button onClick={handleClick} disabled={isPending}>
{/* We show a loading state because we don't have the new data yet */}
{isPending ? "Updating..." : `♥ ${likeCount} Likes`}
</button>
);
}
2. The "Illusion" Approach (useTransition + useOptimistic)
Here, we predict the future. We know the user wants to add a like, so we visually add it immediately. We still use startTransition to keep the app responsive, but we don't wait for the server to update the number.
The Experience: User clicks → Number updates instantly. (No spinner needed).
'use client';
import { useOptimistic, useTransition } from 'react';
import { incrementLike } from './actions';
export default function OptimisticLikeButton({ likeCount }) {
// Setup the Optimistic State
// arg1: The real current data (from server)
// arg2: A reducer function to calculate the "fake" new state
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likeCount,
(currentState, optimisticValue) => currentState + optimisticValue
);
const [isPending, startTransition] = useTransition();
const handleClick = () => {
startTransition(async () => {
// 1. THE ILLUSION: Update the UI immediately
addOptimisticLike(1);
// 2. THE REALITY: Perform the actual server request in the background
// If this fails, React automatically rolls back the optimistic state!
await incrementLike();
});
};
return (
<button onClick={handleClick}>
{/* We display the optimistic (predicted) value immediately */}
♥ {optimisticLikes} Likes
</button>
);
}
Key Differences Summary
| Feature | useTransition |
useOptimistic |
|---|---|---|
| Primary Goal | Prevents UI freezing; tracks "loading" status. | shows the result instantly (before server confirms). |
| User Experience | "Please wait..." (Honest delay) | "Done!" (Instant gratification) |
| Data Source | Waiting for Server Data. | Uses Client-side prediction. |
| If Server Fails? | App usually stays in the previous state or shows an error. | Automatically reverts the UI to the correct server state. |
Why use them together?
You rarely use useOptimistic without useTransition (or Server Actions which wrap transitions).
-
useOptimistichandles the data (showing 101 likes instead of 100). -
useTransitionhandles the execution (making sure the button click doesn't freeze the page while the request travels to the server).
This combination creates the "speed and clean user experience".
Top comments (0)