Responsive AI Loading States
Keep the UI responsive during slow AI calls with optimistic updates, transitions, and clear status states.
TL;DR
- Show the user's message instantly with an
optimisticupdate. - Track a clear
status: idle, loading, streaming, or error. - Keep input snappy by wrapping heavy work in
useTransition.
Show It Optimistically
Add ImmediatelyPush the user's message before the call.
setMessages((m) => [...m, userMsg]);useOptimisticReact 19 tracks the pending message.
const [opt, addOpt] = useOptimistic(messages);Typing IndicatorShow a placeholder while waiting.
{pending && <TypingDots />}Track a Status
Status ValueModel the request as one status value.
type Status = 'idle' | 'loading' | 'error';Drive the UIRender from the status, not booleans.
{status === 'loading' && <Skeleton />}One SourceAvoid several loose isLoading flags.
// one status beats many booleansKeep Input Snappy
useTransitionMark heavy updates as non-urgent.
const [isPending, startTransition] =
useTransition();Wrap the WorkKeep typing instant during updates.
startTransition(() => setResults(next));Show PendingDim the list while it catches up.
<List className={isPending ? 'dim' : ''} />Skeletons and Suspense
Match the ShapeA skeleton mimics the answer layout.
<div className="h-4 w-full animate-pulse" />Suspense FallbackShow a fallback while data loads.
<Suspense fallback={<Skeleton />}>Disable SendBlock resend until the call finishes.
<button disabled={status === 'loading'}>Tips
- Render the user's message and a typing indicator before the request resolves, so the app feels instant while the model thinks.
- Use React 19's
useOptimisticor a single status value to drive the UI instead of juggling several looseisLoadingbooleans.
Warnings
- Do not leave the send button active during a request; a double-click fires two paid calls and can create a
raceCondition. - Avoid a bare
spinnerfor multi-second waits; a skeleton or streaming text reads as far faster to users than a lone spinner.
In Practice
Sending a message shows it immediately and a typing indicator while the AI responds.
- The user's message is added to state before the request starts.
- A busy flag drives a typing indicator and disables the send button.
- The AI reply is appended and busy returns to false.
- The instant echo makes a multi-second call feel conversational.
function useChat() {
const [messages, setMessages] = useState<Msg[]>([]);
const [busy, setBusy] = useState(false);
const send = async (text: string) => {
setMessages((m) => [...m, { role: 'user', text }]);
setBusy(true);
const res = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ text }),
});
const { reply } = await res.json();
setMessages((m) => [...m, { role: 'ai', text: reply }]);
setBusy(false);
};
return { messages, busy, send };
}FAQ
Update the UI before the response arrives. Show the user's message immediately, display a typing indicator, and stream the answer in. This optimistic approach makes the wait feel like progress rather than a frozen screen.
You add the user's message to the list the instant they send, before the server responds. React 19's useOptimistic hook makes this clean, showing the pending state and reconciling once the real response lands.
For short waits a spinner is fine, but AI calls run for seconds. A skeleton that mimics the answer's shape, or streamed text, gives the eye something to track and reads as much faster than a spinning circle.