Back to Blog
ReactuseOptimisticHooksOptimistic UI

React useOptimistic Hook Guide

Master the React useOptimistic hook for optimistic UI updates that enhance perceived performance.

B
Bootspring Team
Engineering
February 12, 2019
6 min read

The useOptimistic hook enables optimistic UI updates that show immediately while async operations complete. Here's how to use it.

Basic Usage#

1import { useOptimistic, useState } from 'react'; 2 3function TodoList({ todos, addTodo }) { 4 const [optimisticTodos, setOptimisticTodos] = useOptimistic( 5 todos, 6 (state, newTodo) => [...state, { ...newTodo, pending: true }] 7 ); 8 9 async function handleSubmit(formData) { 10 const newTodo = { 11 id: Date.now(), 12 text: formData.get('text'), 13 }; 14 15 // Show immediately 16 setOptimisticTodos(newTodo); 17 18 // Actual async operation 19 await addTodo(newTodo); 20 } 21 22 return ( 23 <div> 24 <form action={handleSubmit}> 25 <input name="text" /> 26 <button type="submit">Add</button> 27 </form> 28 29 <ul> 30 {optimisticTodos.map((todo) => ( 31 <li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}> 32 {todo.text} 33 {todo.pending && ' (saving...)'} 34 </li> 35 ))} 36 </ul> 37 </div> 38 ); 39}

Like Button#

1import { useOptimistic } from 'react'; 2 3function LikeButton({ postId, initialLiked, initialCount, onLike }) { 4 const [optimistic, setOptimistic] = useOptimistic( 5 { liked: initialLiked, count: initialCount }, 6 (state, newLiked) => ({ 7 liked: newLiked, 8 count: newLiked ? state.count + 1 : state.count - 1, 9 }) 10 ); 11 12 async function handleClick() { 13 const newLiked = !optimistic.liked; 14 15 // Update UI immediately 16 setOptimistic(newLiked); 17 18 // Sync with server 19 await onLike(postId, newLiked); 20 } 21 22 return ( 23 <button onClick={handleClick}> 24 {optimistic.liked ? 'ā¤ļø' : 'šŸ¤'} {optimistic.count} 25 </button> 26 ); 27}

Message Sending#

1import { useOptimistic, useState } from 'react'; 2 3function Chat({ messages, sendMessage }) { 4 const [optimisticMessages, addOptimisticMessage] = useOptimistic( 5 messages, 6 (state, newMessage) => [ 7 ...state, 8 { 9 ...newMessage, 10 sending: true, 11 }, 12 ] 13 ); 14 15 async function handleSend(formData) { 16 const text = formData.get('message'); 17 const tempId = `temp-${Date.now()}`; 18 19 const newMessage = { 20 id: tempId, 21 text, 22 sender: 'me', 23 timestamp: new Date(), 24 }; 25 26 // Show message immediately 27 addOptimisticMessage(newMessage); 28 29 // Send to server 30 await sendMessage(text); 31 } 32 33 return ( 34 <div className="chat"> 35 <div className="messages"> 36 {optimisticMessages.map((msg) => ( 37 <div 38 key={msg.id} 39 className={`message ${msg.sender} ${msg.sending ? 'sending' : ''}`} 40 > 41 {msg.text} 42 {msg.sending && <span className="status">Sending...</span>} 43 </div> 44 ))} 45 </div> 46 47 <form action={handleSend}> 48 <input name="message" placeholder="Type a message..." /> 49 <button type="submit">Send</button> 50 </form> 51 </div> 52 ); 53}

Delete with Undo#

1import { useOptimistic, useState } from 'react'; 2 3function ItemList({ items, deleteItem }) { 4 const [deletedIds, setDeletedIds] = useState(new Set()); 5 6 const [optimisticItems, setOptimisticItems] = useOptimistic( 7 items.filter((item) => !deletedIds.has(item.id)), 8 (state, deletedId) => state.filter((item) => item.id !== deletedId) 9 ); 10 11 async function handleDelete(id) { 12 // Show deletion immediately 13 setOptimisticItems(id); 14 15 // Start undo timer 16 const undoTimeout = setTimeout(async () => { 17 setDeletedIds((prev) => new Set([...prev, id])); 18 await deleteItem(id); 19 }, 5000); 20 21 // Return undo function 22 return () => clearTimeout(undoTimeout); 23 } 24 25 return ( 26 <ul> 27 {optimisticItems.map((item) => ( 28 <li key={item.id}> 29 {item.name} 30 <button onClick={() => handleDelete(item.id)}>Delete</button> 31 </li> 32 ))} 33 </ul> 34 ); 35}

Form Submission#

1import { useOptimistic, useActionState } from 'react'; 2 3function CommentForm({ postId, comments, addComment }) { 4 const [optimisticComments, addOptimisticComment] = useOptimistic( 5 comments, 6 (state, newComment) => [ 7 ...state, 8 { ...newComment, pending: true }, 9 ] 10 ); 11 12 async function submitComment(formData) { 13 const comment = { 14 id: `temp-${Date.now()}`, 15 text: formData.get('comment'), 16 author: 'Current User', 17 createdAt: new Date(), 18 }; 19 20 addOptimisticComment(comment); 21 22 await addComment(postId, comment.text); 23 } 24 25 return ( 26 <div> 27 <ul className="comments"> 28 {optimisticComments.map((comment) => ( 29 <li 30 key={comment.id} 31 className={comment.pending ? 'pending' : ''} 32 > 33 <strong>{comment.author}</strong> 34 <p>{comment.text}</p> 35 {comment.pending && <span>Posting...</span>} 36 </li> 37 ))} 38 </ul> 39 40 <form action={submitComment}> 41 <textarea name="comment" required /> 42 <button type="submit">Post Comment</button> 43 </form> 44 </div> 45 ); 46}

Toggle with Multiple States#

1import { useOptimistic } from 'react'; 2 3function BookmarkButton({ itemId, status, onToggle }) { 4 // status: 'none' | 'bookmarked' | 'favorited' 5 const [optimisticStatus, setOptimisticStatus] = useOptimistic( 6 status, 7 (_, newStatus) => newStatus 8 ); 9 10 async function handleToggle(newStatus) { 11 setOptimisticStatus(newStatus); 12 await onToggle(itemId, newStatus); 13 } 14 15 return ( 16 <div className="bookmark-actions"> 17 <button 18 className={optimisticStatus === 'bookmarked' ? 'active' : ''} 19 onClick={() => 20 handleToggle(optimisticStatus === 'bookmarked' ? 'none' : 'bookmarked') 21 } 22 > 23 šŸ”– Bookmark 24 </button> 25 <button 26 className={optimisticStatus === 'favorited' ? 'active' : ''} 27 onClick={() => 28 handleToggle(optimisticStatus === 'favorited' ? 'none' : 'favorited') 29 } 30 > 31 ⭐ Favorite 32 </button> 33 </div> 34 ); 35}

Cart Updates#

1import { useOptimistic } from 'react'; 2 3function CartItem({ item, updateQuantity, removeItem }) { 4 const [optimisticItem, setOptimisticItem] = useOptimistic( 5 item, 6 (state, update) => ({ 7 ...state, 8 ...update, 9 updating: true, 10 }) 11 ); 12 13 async function handleQuantityChange(newQuantity) { 14 if (newQuantity === 0) { 15 setOptimisticItem({ removed: true }); 16 await removeItem(item.id); 17 } else { 18 setOptimisticItem({ quantity: newQuantity }); 19 await updateQuantity(item.id, newQuantity); 20 } 21 } 22 23 if (optimisticItem.removed) { 24 return null; 25 } 26 27 return ( 28 <div className={`cart-item ${optimisticItem.updating ? 'updating' : ''}`}> 29 <span>{item.name}</span> 30 <div className="quantity-controls"> 31 <button onClick={() => handleQuantityChange(optimisticItem.quantity - 1)}> 32 - 33 </button> 34 <span>{optimisticItem.quantity}</span> 35 <button onClick={() => handleQuantityChange(optimisticItem.quantity + 1)}> 36 + 37 </button> 38 </div> 39 <span>${(item.price * optimisticItem.quantity).toFixed(2)}</span> 40 </div> 41 ); 42}

Error Handling#

1import { useOptimistic, useState } from 'react'; 2 3function EditableItem({ item, updateItem }) { 4 const [error, setError] = useState(null); 5 6 const [optimisticItem, setOptimisticItem] = useOptimistic( 7 item, 8 (state, newValue) => ({ ...state, value: newValue, saving: true }) 9 ); 10 11 async function handleSave(newValue) { 12 setError(null); 13 setOptimisticItem(newValue); 14 15 try { 16 await updateItem(item.id, newValue); 17 } catch (err) { 18 setError(err.message); 19 // The optimistic update will be reverted when 20 // the actual item prop updates 21 } 22 } 23 24 return ( 25 <div> 26 <input 27 value={optimisticItem.value} 28 onChange={(e) => handleSave(e.target.value)} 29 disabled={optimisticItem.saving} 30 /> 31 {optimisticItem.saving && <span>Saving...</span>} 32 {error && <span className="error">{error}</span>} 33 </div> 34 ); 35}

Batch Operations#

1import { useOptimistic } from 'react'; 2 3function BulkActions({ items, selectedIds, onBulkDelete, onBulkArchive }) { 4 const [optimisticItems, setOptimisticItems] = useOptimistic( 5 items, 6 (state, action) => { 7 switch (action.type) { 8 case 'delete': 9 return state.filter((item) => !action.ids.includes(item.id)); 10 case 'archive': 11 return state.map((item) => 12 action.ids.includes(item.id) 13 ? { ...item, archived: true, pending: true } 14 : item 15 ); 16 default: 17 return state; 18 } 19 } 20 ); 21 22 async function handleBulkDelete() { 23 setOptimisticItems({ type: 'delete', ids: [...selectedIds] }); 24 await onBulkDelete([...selectedIds]); 25 } 26 27 async function handleBulkArchive() { 28 setOptimisticItems({ type: 'archive', ids: [...selectedIds] }); 29 await onBulkArchive([...selectedIds]); 30 } 31 32 return ( 33 <div> 34 <div className="actions"> 35 <button onClick={handleBulkDelete} disabled={selectedIds.size === 0}> 36 Delete Selected 37 </button> 38 <button onClick={handleBulkArchive} disabled={selectedIds.size === 0}> 39 Archive Selected 40 </button> 41 </div> 42 43 <ul> 44 {optimisticItems.map((item) => ( 45 <li key={item.id} className={item.pending ? 'pending' : ''}> 46 {item.name} 47 </li> 48 ))} 49 </ul> 50 </div> 51 ); 52}

Best Practices#

When to Use: āœ“ Likes/favorites/bookmarks āœ“ Adding items to lists āœ“ Form submissions āœ“ Toggle states āœ“ Real-time updates Patterns: āœ“ Show pending state visually āœ“ Handle errors gracefully āœ“ Revert on failure āœ“ Use with server actions Visual Feedback: āœ“ Reduce opacity for pending āœ“ Show loading indicators āœ“ Disable during update āœ“ Clear error states Avoid: āœ— Critical data without confirmation āœ— Complex multi-step operations āœ— Operations that can't be reverted āœ— Ignoring error states

Conclusion#

The useOptimistic hook provides immediate UI feedback while async operations complete in the background. Use it for actions like likes, adding items, and form submissions where instant feedback improves perceived performance. Always provide visual indicators for pending states and handle errors gracefully by reverting the UI when operations fail. Combine with server actions for a seamless optimistic update pattern.

Share this article

Help spread the word about Bootspring