Compound Components: Truly Flexible React APIs
Build flexible, declarative UIs like native HTML with Compound Components—a powerful React pattern you’ll master at EpicReact.dev.

When you click a button in your app, how long does it take for the UI to respond? If you're waiting for a network request to finish before showing the result, your users might be left staring at a spinner, wondering if anything happened at all.
That's where optimistic UI comes in. The idea is simple: assume the best, update the UI immediately, and let the server catch up. If something goes wrong, you can always roll back. But most of the time, things go right—and your app feels lightning fast.
React 19 introduced a new hook,
useOptimistic
, that makes
this pattern easier than ever.
useOptimistic
?useOptimistic
is a React Hook that lets you show a different state while an
async action is underway. It's perfect for things like instantly adding a new
item to a list when the user submits a form allowing them to quickly add another
item or even change the list item before it's even actually saved. You can also
give users feedback that their action is being processed and even updating the
UI to show individual parts of progress. For example: "reserving your seat" ->
"creating calendar event" -> "sending invitations" -> "done".
Let's see how it works in practice.
Let's try another example: a "Like" button. When a user clicks it, you want the count to update right away.
import { useState, useOptimistic, startTransition } from 'react'function LikeButton({ initialCount, sendLike }) { const [count, setCount] = useState(initialCount) // The update function adds the optimistic delta to the count const [optimisticCount, addOptimisticLike] = useOptimistic( count, (current, delta) => current + delta, ) function handleLike() { // Optimistically increment the count addOptimisticLike(1) // Actually send to the server using the provided async function startTransition(async () => { await sendLike() setCount((c) => c + 1) }) } return <button onClick={handleLike}>👍 {optimisticCount}</button>}
The user sees the like count go up immediately, even if the network is slow.
Suppose you have a comment section. When a user submits a new comment, you want it to appear instantly—even before the server responds.
Here's how you might do it with useOptimistic
:
import { useState, useOptimistic, startTransition } from 'react'function CommentSection({ initialComments, sendComment }) { const [comments, setComments] = useState(initialComments) // The update function merges the optimistic comment into the list const [optimisticComments, addOptimisticComment] = useOptimistic( comments, (currentComments, newComment) => [ { ...newComment, optimistic: true }, ...currentComments, ], ) async function handleAddComment(text) { // Show the comment immediately addOptimisticComment({ id: Date.now(), text }) // Actually send to the server using the provided async function startTransition(async () => { const savedComment = await sendComment(text) setComments((prev) => [{ ...savedComment }, ...prev]) }) } return ( <div> <form action={async (formData) => { handleAddComment(formData.get('text')) }} > <input name="text" placeholder="Write a comment..." /> <button type="submit">Add</button> </form> <ul> {optimisticComments.map((comment) => ( <li key={comment.id} className={comment.optimistic ? 'text-gray-500' : ''} > {comment.text} </li> ))} </ul> </div> )}
Notice how the new comment appears instantly, with a grayed out text, even before the server responds. If there was an error, then the comment will go away at the end of the transition (you can show an error message if you want). If there was no error, then the optimistic comment is still gone at the end of the transition, but the newly created comment will be in the list of regular comments (and will no longer be grayed out).
Suppose you want to let users book a flight, and you want to show progress through multiple steps—like "reserving seat", "processing payment", and "sending confirmation"—with optimistic UI updates for both the booking state and the progress message. You can accept all async logic and step messages as props, making the component flexible and focused on UI state.
import { useOptimistic, startTransition } from 'react'import { reserveSeat, processPayment, sendConfirmation } from './api'function BookFlight() { const [message, setMessage] = useOptimistic('Ready to book') async function handleBooking(formData: FormData) { setMessage('Reserving seat...') await reserveSeat(formData.get('flight')) setMessage('Processing payment...') await processPayment(formData.get('passenger')) setMessage('Sending confirmation...') const bookingId = await sendConfirmation( formData.get('passenger'), formData.get('flight'), ) setMessage('Booking complete! Redirecting...') // In a real app, you'd use your routing library here console.log(`Redirecting to /booking/${bookingId}`) } return ( <form action={handleBooking}> <input name="passenger" placeholder="Passenger Name" required /> <input name="flight" placeholder="Flight Number" required /> <button type="submit">Book Flight</button> <div className="mt-2"> <strong>Status:</strong> {message} </div> </form> )}
This pattern lets you keep all business logic outside the component, while still providing a rich, step-by-step optimistic UI experience for your users.
useState
?You might wonder: why not just update state directly and hope for the best? The
difference is that useOptimistic
is designed to work seamlessly with React's
concurrent rendering and transitions. It keeps your UI in sync with the "real"
state, and automatically falls back if the action fails or completes.
Additionally, calling setState
in a transition does not trigger a re-render
due to the nature of React transitions.
useOptimistic
useOptimistic
with startTransition
ensures your UI stays responsive, even during async work.Optimistic UI is one of those little touches that can make your app feel
magical. With useOptimistic
, React gives you a simple, powerful tool to make
your interfaces feel instant—even when the network isn't.
So next time you're building a form, a like button, or anything that talks to a
server, give useOptimistic
a try. Your users will thank you.
If you enjoyed this article and want to master hands-on patterns like
useOptimistic
, Suspense, and more, join me in the Epic React Suspense
workshop! You'll get practical experience, real-world exercises, and a deeper
understanding of modern React.
Register for Epic React and take your React skills to the next level!
Delivered straight to your inbox.
Build flexible, declarative UIs like native HTML with Compound Components—a powerful React pattern you’ll master at EpicReact.dev.
Speed up your app's loading of code/data/assets with "render as you fetch" with and without React Suspense for Data Fetching
Epic React is your learning spotlight so you can ship harder, better, faster, stronger.
Truly maintainable, flexible, simple, and reusable components require more thought than: "I need it to do this differently, so I'll accept a new prop for that". Seasoned React developers know this leads to nothing but pain and frustration for both the maintainer and user of the component.