Am I the only one who finds useOptimistic
both fascinating and amazing? If you're curious and want to dive deep into how to use this hook, you're in the right place.
đź’ˇ useOptimistic hook, is someone who always starts a band, forever hopeful that the next hit will be a success.
The useOptimistic
hook is a React hook that lets you show a different state when an asynchronous action is processing. If the action fails, the state can be reverted to its previous state. I like to think of it as a client that knows more than the server. Instead of waiting for a response from the server, we can use the optimistic state to update the UI while the asynchronous action is underway. This makes the app feel faster and more responsive to the user.
useOptimistic
is only available on React 19. The example in this writing make use of Nextjs 14.
Scenario Example: Imagine a comment section where you can optimistically update the comment list to show the latest comment. We can immediately display the new comment instead of waiting for the roundtrip to the server. If the server request fails, the previous state is restored, and possibly an indicator is shown to the user that the comment update has failed.
Always implement a rollback mechanism and provide user feedback if the asynchronous operation fails. This enhances the user experience.
useOptimistic(state, updateFn)
The useOptimistic takes in two parameters state, updateFn.
Parameters:
- initialState: This is the initial state.
- updateFn: A function used to apply the optimistic update to the state.
Returns:
-
optimisticState: The optimistic state returned by
updateFn
. - addOptimistic: A function used to update the optimistic state.
More Insights on Parameters and Returned Values:
To understand how to use useOptimistic
, we need to explore how the parameters and return values fit together.
Parameters:
-
initialState
- Type: It can be of any type (string, number, array, boolean, etc.).
- Purpose: This is the initial state that your component starts with. It is the baseline before any optimistic update is applied.
-
Example: If you're managing a list of comments,
initialState
can be an array fetched from your database.
const commentList = [{ id: 1, comment: "This is a comment" }, {id: 2, comment: "This is another comment" }]
- updateFn
-
Type:
(currentState: StateType, newOptimisticValue: any) => StateType
- Purpose: This function defines how the state should update. It receives the current state and the value you want to optimistically update (e.g., a new comment) and returns the updated state.
const updateFn = (currentState, newValues) => [...currentState, newValues]
This function should be pure, meaning it should not cause any side effects and should return the same state based on the input.
Return Values:
-
optimisticState
-
Type: Same as
initialState
. -
Purpose: This represents the current state of the component after the optimistic update has been applied. As you apply an optimistic update using the
addOptimistic
function,optimisticState
reflects the changes made. - Example
-
Type: Same as
const [optimisticState, addOptimistic] = useOptimistic(commentList, (currentState, newComment) => [...currentState, newComment])
return (
<ul>
{optimisticState.map((comment) => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
);
addOptimistic
-
Type:
(newOptimisticValue: any) => void
-
Purpose: This function is used to apply an optimistic update to the state. When you call this function, it triggers
updateFn
with the current state and the new optimistic value, returning the new state. - Example
const newComment = { id: 3, comment: "Add new comment" }
addOptimistic(newComment);
That was a long overview, but I hope you learned something about how the pieces fit together. If not, feel free to go over it again. Having a solid foundation will provide a blueprint for how to use this hook effectively.
Optimistic Shopping List
We'll create a shopping list that adds items to the cart optimistically. We'll use JavaScript Promises and setTimeout
to simulate a network request and a custom function to rollback the previous data if the request fails.
// here is our initial data and type
type Item = {
id: number;
name: string;
};
const initialState: Item[] = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Orange" },
{ id: 3, name: "Cherry" },
{ id: 4, name: "Pineapple" },
{ id: 5, name: "Grape" },
];
Since we don’t have a server setup, we’ll simulate a server response using JavaScript Promise and setTimeout
.
// Server simulation function
// NOTE: Do not put this function into a component
// to avoid re-creation on each render
async function simulateServer(cartItem: Item[], item: Item) {
return new Promise<Item[]>((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.3) {
resolve([...cartItem, item]);
} else {
{
reject(new Error("Failed"));
}
}
}, 3000);
});
}
In our simulation, if the Math.random()
value is greater than 0.3, the function simulates an OK response and adds new items to the cart. Otherwise, the response fails by calling the reject
function.
// here is the shoppingList component
export default function ShoppingList() {
const [cartItem, setCartItem] = useState<Item[]>([]);
const [error, setError] = useState<string>("");
const [optimisticState, addOptimistic] = useOptimistic<Item[], Item>(
cartItem,
(currentState, newValue) => [...currentState, newValue]
);
async function addNewItem(item: Item) {
startTransition(() => { // `startTransition` for non-urgent updates that shouldn't block the user interface.
addOptimistic(item);
});
try {
const newItem = await simulateServer(optimisticState, item);
setCartItem(newItem);
} catch (error) {
setError(`Failed to add item ${item.name}`);
rollbackOptimisticUpdate(item.id);
}
}
useEffect(() => {
if (error) {
const timer = setTimeout(() => setError(""), 3000);
return () => clearTimeout(timer);
}
}, [error]);
// rollback the previous data if request failed
function rollbackOptimisticUpdate(tempId: number) {
startTransition(() => {
const revertItem = cartItem.filter((item) => item.id !== tempId)
setCartItem(revertItem);
revertItem.forEach(item => addOptimistic(item))
});
}
return (
<>
<div className="flex gap-20 justify-between">
<ul>
{initialState.map((item: Item) => {
return (
<li
key={item.id}
className="flex gap-4 justify-between font-semibold p-4 border border-slate-100 "
>
{item.name}
<button
onClick={() => addNewItem(item)}
className="px-3 py-2 rounded-md hover:bg-slate-400 bg-slate-100 text-black"
>
Add
</button>
</li>
);
})}
</ul>
<div className="relative">
<FaCartShopping size={100} />
<div className="absolute top-4 inset-0 ">
<p className="text-3xl text-slate-800 text-center font-bold">
{optimisticState.length}
</p>
</div>
</div>
<p className=" text-red-300 font-semibold">{error}</p>
</div>
</>
);
}
-
addNewItem
function handles the addition of items to the cart.- It triggers an optimistic update using
addOptimistic
, adding the new item tooptimisticState
without waiting for a server response. - It then attempts to upadte the server by calling
simulateServer
. If the server responds successfully, thecartItem
state is updated with the confirmed data. - If the server request fails, the error is captured, and the
rollbackOptimisticUpdate
function is called to remove the optimistic update.
- It triggers an optimistic update using
- The
rollbackOptimisticUpdate
function usesaddOptimistic
to filter the item that failed to update, effectively rolling back the optimistic state to what it was before the failed operation.
Conclusion
Optimistic way of rendering data, coupled with rollback mechanism, ensures that users get a responsive feel like interaction and quick feedback if any error occurs, while maintaining a smooth interaction flow. Leveraging useOptimistic
can significantly enhance the perceived performance of your React application.
Top comments (0)