Handling Optimistic Updates in Redux Toolkit (Without Third-Party Libraries)
Waiting for server confirmations before updating the UI feels sluggish. In real-world apps, users expect near-instant feedback. Enter optimistic updates: immediately reflect changes in the UI while the server catches up. Redux Toolkit makes this surprisingly clean — no extra libraries needed. What Are Optimistic Updates? Instead of waiting for a network response, you assume the action will succeed and update the UI instantly. If the server later responds with an error, you roll back or show a correction. Step 1: Structure the Slice for Pending States Let's model an optimistic update for a "like" feature: // src/features/posts/postsSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import axios from 'axios'; export const likePost = createAsyncThunk( 'posts/likePost', async (postId) => { await axios.post(`/api/posts/${postId}/like`); return postId; } ); const postsSlice = createSlice({ name: 'posts', initialState: { posts: [], error: null, }, reducers: { optimisticLike(state, action) { const post = state.posts.find(p => p.id === action.payload); if (post) { post.likes++; } }, revertLike(state, action) { const post = state.posts.find(p => p.id === action.payload); if (post) { post.likes--; } }, }, extraReducers: (builder) => { builder .addCase(likePost.fulfilled, (state, action) => { // No-op because UI already updated optimistically }) .addCase(likePost.rejected, (state, action) => { state.error = 'Failed to like post'; }); }, }); export const { optimisticLike, revertLike } = postsSlice.actions; export default postsSlice.reducer; Step 2: Dispatch in the Right Order In your React component: // src/features/posts/LikeButton.js import { useDispatch } from 'react-redux'; import { optimisticLike, revertLike, likePost } from './postsSlice'; function LikeButton({ postId }) { const dispatch = useDispatch(); const handleLike = () => { dispatch(optimisticLike(postId)); dispatch(likePost(postId)).unwrap().catch(() => { dispatch(revertLike(postId)); }); }; return ( Like ); } How It Works First, optimisticLike immediately increments likes in the local state. Then, likePost sends the actual API request. If the request fails, revertLike corrects the UI by decrementing. Pros and Cons ✅ Pros Instant feedback for users Very little code overhead with Redux Toolkit Decouples UI responsiveness from server performance ⚠️ Cons Edge cases if the server state diverges (e.g., data race conditions) Rollback logic can become complex for more critical operations
Waiting for server confirmations before updating the UI feels sluggish. In real-world apps, users expect near-instant feedback. Enter optimistic updates: immediately reflect changes in the UI while the server catches up. Redux Toolkit makes this surprisingly clean — no extra libraries needed.
What Are Optimistic Updates?
Instead of waiting for a network response, you assume the action will succeed and update the UI instantly. If the server later responds with an error, you roll back or show a correction.
Step 1: Structure the Slice for Pending States
Let's model an optimistic update for a "like" feature:
// src/features/posts/postsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
export const likePost = createAsyncThunk(
'posts/likePost',
async (postId) => {
await axios.post(`/api/posts/${postId}/like`);
return postId;
}
);
const postsSlice = createSlice({
name: 'posts',
initialState: {
posts: [],
error: null,
},
reducers: {
optimisticLike(state, action) {
const post = state.posts.find(p => p.id === action.payload);
if (post) {
post.likes++;
}
},
revertLike(state, action) {
const post = state.posts.find(p => p.id === action.payload);
if (post) {
post.likes--;
}
},
},
extraReducers: (builder) => {
builder
.addCase(likePost.fulfilled, (state, action) => {
// No-op because UI already updated optimistically
})
.addCase(likePost.rejected, (state, action) => {
state.error = 'Failed to like post';
});
},
});
export const { optimisticLike, revertLike } = postsSlice.actions;
export default postsSlice.reducer;
Step 2: Dispatch in the Right Order
In your React component:
// src/features/posts/LikeButton.js
import { useDispatch } from 'react-redux';
import { optimisticLike, revertLike, likePost } from './postsSlice';
function LikeButton({ postId }) {
const dispatch = useDispatch();
const handleLike = () => {
dispatch(optimisticLike(postId));
dispatch(likePost(postId)).unwrap().catch(() => {
dispatch(revertLike(postId));
});
};
return (
);
}
How It Works
- First,
optimisticLike
immediately increments likes in the local state. - Then,
likePost
sends the actual API request. - If the request fails,
revertLike
corrects the UI by decrementing.
Pros and Cons
✅ Pros
- Instant feedback for users
- Very little code overhead with Redux Toolkit
- Decouples UI responsiveness from server performance
⚠️ Cons
- Edge cases if the server state diverges (e.g., data race conditions)
- Rollback logic can become complex for more critical operations