Lately, I’ve been thinking a lot about a problem that seems to pop up in every serious React project I work on. It starts simply enough: you fetch some data from an API and display it. Then you add a filter. Then a search bar. Suddenly, your components are a tangled mess of loading states, error checks, cached data, and UI toggles. The state management feels heavy, and adding a simple feature becomes a chore. I found myself asking, why do we often use one tool to solve two completely different problems? This led me to a powerful combination: using Zustand and React Query together.
Think about your own applications. How do you separate the data that lives on your server from the state that only exists in the user’s browser? They are fundamentally different, yet we often store them in the same place.
The core issue is mixing concerns. Server state—data from an API—is about synchronization, caching, and freshness. It’s shared, asynchronous, and owned by a backend. Client state—like whether a modal is open or which tab is selected—is local, synchronous, and owned entirely by the UI. When you use a single global store for both, your simple UI actions get bogged down in cache logic, and your data fetching library gets cluttered with UI flags.
This is where a clear separation of duties changes everything.
Let’s meet the specialists. For local, client-side state, I’ve grown fond of Zustand. It’s incredibly simple. You create a store with a function. There’s no boilerplate, no providers to wrap your app in unless you want them. It feels like using React’s built-in state, but available anywhere.
import { create } from 'zustand';
const useUiStore = create((set) => ({
isDarkMode: false,
activeTab: 'dashboard',
toggleDarkMode: () => set((state) => ({ isDarkMode: !state.isDarkMode })),
setActiveTab: (tab) => set({ activeTab: tab }),
}));
Using it in a component is straightforward. You select only the state you need, and the component will re-render only when that specific piece changes.
For everything that comes from a server, React Query is the expert. It’s not a state management library; it’s a server-state manager. It handles the heavy lifting you’d otherwise write yourself: caching, background refetches, pagination, and managing loading and error states. It turns complex data flow into a declarative query.
import { useQuery } from '@tanstack/react-query';
const fetchUsers = async () => {
const response = await fetch('/api/users');
return response.json();
};
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
// React Query manages caching, retries, and more.
}
So, how do they work together? They form a clean, reactive pipeline. The client state in Zustand can drive queries in React Query. Imagine a user dashboard. You might store filter settings—like a date range or a selected category—in a Zustand store because that’s pure UI preference. When those filters change, you want to refetch the data.
The magic happens when you connect them. A change in the Zustand store can become a dependency for a React Query queryKey. When the key changes, React Query automatically knows to fetch fresh data.
import { useQuery } from '@tanstack/react-query';
import useDashboardStore from '../stores/dashboardStore';
// A Zustand store for our filters
// create((set) => ({ startDate: null, endDate: null, category: 'all', ... }))
function useDashboardData() {
const { startDate, endDate, category } = useDashboardStore();
return useQuery({
// The queryKey includes our client state. Changing the state invalidates the query.
queryKey: ['dashboard', startDate, endDate, category],
queryFn: () => fetchDashboardData({ startDate, endDate, category }),
});
}
Have you ever had a filter change that required resetting pagination? This pattern handles that elegantly. The Zustand store holds the current page, and when the filter changes, you can reset the page to 1 in the same store update. Because the page number is also in the queryKey, React Query will fetch page 1 of the new filtered results automatically.
What about updating local state after a server action? Let’s say you delete an item. After a successful mutation with React Query, you can invalidate the related query to trigger a refetch. But you might also need to close a modal or clear a selection in your UI. That’s a simple call to your Zustand store.
const useTaskStore = create((set) => ({
selectedTaskId: null,
clearSelection: () => set({ selectedTaskId: null }),
}));
function useDeleteTask() {
const clearSelection = useTaskStore((state) => state.clearSelection);
const mutation = useMutation({
mutationFn: deleteTask,
onSuccess: () => {
// Invalidate server cache
queryClient.invalidateQueries({ queryKey: ['tasks'] });
// Update local UI state
clearSelection();
},
});
return mutation;
}
The beauty is in the simplicity. Zustand stays small and focused on what the user is doing right now. React Query manages the complex lifecycle of the data that powers the experience. Each is responsible for what it does best. Your components become simpler because they just consume these focused hooks.
This approach scales. New features often mean adding a new query or a new slice to a store, not re-architecting a monolithic state object. Testing is easier because you can test UI logic and server synchronization independently. Can you see how separating these concerns might simplify your own codebase?
To get started, I recommend adding them separately. Use React Query for your next data fetch. Use Zustand for the next UI control panel or settings modal. You’ll quickly see their individual strengths. Then, look for the connections—a filter here, a form state there. That’s where you link them, creating a responsive, efficient flow.
I’ve found this combination to be a game-changer for building maintainable, fast React applications. It respects the different nature of client and server state, and gives each the dedicated tool it deserves. The result is code that’s easier to write, reason about, and grow over time.
If this approach to managing state resonates with you, or if you have a different strategy that works, I’d love to hear about it. What has your experience been? Please feel free to share your thoughts in the comments below, and if you found this useful, consider sharing it with other developers who might be facing the same architectural challenges. Let’s build better React applications together.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva