Optimistic concurrency is a powerful technique that enhances user experience by providing instant feedback before an operation is confirmed by the server. Before React 19 introduced useOptimistic, developers had to rely on various workarounds to achieve this behavior.
In this article, we will explore the most common approaches used in older versions of React to handle optimistic updates, their advantages, and their limitations.
π What is Optimistic Concurrency?
Optimistic concurrency assumes that an operation will succeed and updates the UI immediately without waiting for server confirmation. If the operation fails, the UI is rolled back to the previous state.
π Approaches Before useOptimistic
1οΈβ£ Using useState for Temporary UI Updates
One of the simplest ways to implement optimistic updates in React is by using useState. Hereβs how it works:
Example: Optimistic Update with useState
import React, { useState } from 'react';
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React' },
{ id: 2, text: 'Build a Project' },
]);
function handleAddTodo() {
const newTodo = { id: Date.now(), text: 'New Todo' };
// Optimistically update UI
setTodos((prev) => [...prev, newTodo]);
// Simulate API call
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
}).catch(() => {
// Rollback on error
setTodos((prev) => prev.filter((todo) => todo.id !== newTodo.id));
});
}
return (
<div>
<button onClick={handleAddTodo}>Add Todo</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
β Pros:
Simple and easy to implement
Provides immediate feedback to the user
β Cons:
Manual rollback is required if the API call fails
Causes unnecessary re-renders when state updates
Difficult to scale for complex state management
2οΈβ£ Using React Queryβs onMutate
For applications that rely on external APIs, React Query was a popular choice before useOptimistic. React Query provides a way to optimistically update the UI and roll back changes on failure.
Example: React Query Optimistic Update
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
function useAddTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newTodo) => axios.post('/api/todos', newTodo),
onMutate: async (newTodo) => {
// Cancel any outgoing queries
await queryClient.cancelQueries(['todos']);
// Snapshot of previous todos
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update UI
queryClient.setQueryData(['todos'], (oldTodos) => [...oldTodos, newTodo]);
return { previousTodos };
},
onError: (_, __, context) => {
// Rollback to previous state on error
queryClient.setQueryData(['todos'], context.previousTodos);
},
});
}
β Pros:
Automatically manages API state
Reduces unnecessary API calls
Built-in rollback mechanism
β Cons:
Requires an external library
More complex to set up
Still involves direct mutation of the cache
3οΈβ£ Using Redux for Optimistic Updates
Before React Query became popular, Redux was widely used to manage global state. Optimistic updates in Redux involved updating the store immediately and dispatching a rollback action if the API call failed.
Example: Optimistic Update with Redux
import { createSlice, configureStore } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodoOptimistic: (state, action) => {
state.push(action.payload);
},
rollbackTodo: (state, action) => {
return state.filter(todo => todo.id !== action.payload.id);
},
},
});
const store = configureStore({ reducer: { todos: todosSlice.reducer } });
// Simulate API call in a component
function handleAddTodo() {
const newTodo = { id: Date.now(), text: 'New Todo' };
store.dispatch(todosSlice.actions.addTodoOptimistic(newTodo));
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
}).catch(() => {
store.dispatch(todosSlice.actions.rollbackTodo(newTodo));
});
}
β Pros:
Works well for complex global state management
Redux DevTools make debugging easier
β Cons:
Requires boilerplate code
Overhead for simple state management
π― How useOptimistic Improves This
React 19βs useOptimistic provides a much cleaner and simpler way to handle optimistic updates. Instead of modifying state directly, useOptimistic allows you to derive a temporary optimistic state without affecting the actual state.
Example: useOptimistic (React 19)
import React, { useState, useOptimistic } from 'react';
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React 19' },
]);
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo]
);
function handleAddTodo() {
const newTodo = { id: Date.now(), text: 'New Todo' };
addOptimisticTodo(newTodo);
setTodos((prev) => [...prev, newTodo]);
}
return (
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
β Benefits of useOptimistic:
No direct state mutation
UI updates instantly without re-rendering
No manual rollback needed
π Conclusion
Before React 19, optimistic concurrency was achieved through useState, React Query, or Redux. Each method had its drawbacks, such as manual rollbacks, direct state mutations, and additional dependencies. With useOptimistic, React now provides a built-in, cleaner, and more efficient way to handle optimistic updates, making UI interactions smoother than ever.
Top comments (0)