Zustand Async: 5 Effective Ways to Handle Async Operations in React
Every real-world React app has to communicate with asynchronous APIs. If managed poorly, your app can run into issues like a frozen UI, race conditions, memory leaks, or showing stale data. Zustand solves this with an incredibly simple syntax — define async actions directly in the store, without complex middleware like Redux Thunk or Saga.

Trung Vũ Hoàng
Author
1. Why Do You Need Centralized Async State Management?
Each API request needs to track at least 3 states: loading, success, error. If this logic is scattered across components, the codebase quickly becomes hard to read and difficult to debug. Centralizing async state in a Zustand store helps:
Fetch logic is completely separated from UI components.
Multiple components share a single source of truth.
It’s easier to handle race conditions and cancel requests when a component unmounts.
2. Method 1 — Basic Async Store
An async action in Zustand is just a normal async function inside the store:
import { create } from 'zustand';
const useUserStore = create((set) => ({
user: null,
isLoading: false,
error: null,
fetchUser: async (userId) => {
set({ isLoading: true, error: null });
try {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Unable to load data!');
const data = await res.json();
set({ user: data, isLoading: false });
} catch (err) {
set({ error: err.message, isLoading: false });
}
},
}));3. Method 2 — Use It in a Component with Selectors
Subscribe only to the exact slice of state you need to avoid unnecessary re-renders:
const UserProfile = ({ userId }) => {
const { user, isLoading, error, fetchUser } = useUserStore(state => ({
user: state.user,
isLoading: state.isLoading,
error: state.error,
fetchUser: state.fetchUser,
}));
useEffect(() => {
fetchUser(userId);
}, [userId]);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <div>{user?.name}</div>;
};4. Method 3 — Run Multiple Requests in Parallel
Use Promise.all inside an action to fetch multiple sources at the same time:
fetchDashboard: async () => {
set({ isLoading: true });
try {
const [stats, users, orders] = await Promise.all([
fetch('/api/stats').then(r => r.json()),
fetch('/api/users').then(r => r.json()),
fetch('/api/orders').then(r => r.json()),
]);
set({ stats, users, orders, isLoading: false });
} catch (err) {
set({ error: err.message, isLoading: false });
}
},5. Method 4 — Optimistic Update
Update the UI immediately, then roll back if the API fails:
addItem: async (item) => {
// Update UI first
set(state => ({ items: [...state.items, item] }));
try {
await fetch('/api/items', { method: 'POST', body: JSON.stringify(item) });
} catch (err) {
// Roll back on error
set(state => ({ items: state.items.filter(i => i.id !== item.id) }));
}
},6. Method 5 — Cancel Requests When They’re No Longer Needed
Use AbortController to cancel a fetch when the component unmounts or when userId changes:
fetchUser: async (userId, signal) => {
set({ isLoading: true });
try {
const res = await fetch(`/api/users/${userId}`, { signal });
const data = await res.json();
set({ user: data, isLoading: false });
} catch (err) {
if (err.name !== 'AbortError') {
set({ error: err.message, isLoading: false });
}
}
},
// In the component:
useEffect(() => {
const controller = new AbortController();
fetchUser(userId, controller.signal);
return () => controller.abort(); // Cancel on unmount
}, [userId]);7. Optimize with Middleware
devtools: Integrate Redux DevTools — debug async actions easily.
immer: Mutable-looking state update syntax while keeping immutability under the hood.
persist: Save state to localStorage and restore it when the page reloads.
Summary
Zustand makes handling async in React more lightweight and explicit than any other solution:
Basic: async functions directly in the store.
Performance: Selectors to avoid unnecessary re-renders.
Parallel: Promise.all inside actions.
Better UX: Optimistic updates + rollback.
Safety: AbortController to prevent memory leaks.
Frequently Asked Questions
Bài viết liên quan

Zustand Async: 5 Effective Ways to Handle Async in React
Every real-world React app needs to communicate with async APIs. If handled poorly, it’s easy to run into issues like frozen UI, race conditions, memory leaks, or showing stale data. Zustand solves this with an extremely simple syntax—define async actions directly in the store, without complex middleware like Redux Thunk or Saga.

Advanced Promises: all, allSettled, race, any — When to Use Which?
If you only use sequential async/await, your app is wasting performance potential — each request has to wait for the previous one to finish before it can start. Promise’s static methods (all, allSettled, race, any) let you orchestrate multiple async tasks in parallel using the strategy that fits each problem.

Zustand Async: 5 Effective Ways to Handle Async Operations in React
Every real-world React app has to communicate with asynchronous APIs. If managed poorly, your app can run into issues like a frozen UI, race conditions, memory leaks, or showing stale data. Zustand solves this with an incredibly simple syntax — define async actions directly in the store, without complex middleware like Redux Thunk or Saga.