I’ve been thinking a lot about state management lately. In my projects, I’ve watched simple components turn into complex webs of useState, useEffect, and context providers. The hardest part isn’t writing the code—it’s knowing where to put things. Should this piece of data live globally? Is it only relevant to this server request? This confusion is why the pairing of Zustand and React Query has become so important to me. Let’s talk about how these two tools, when used together, can bring clarity and power to your application’s architecture.
Think about the different kinds of information in your app. You have data that comes from an API, like a user’s profile or a list of products. This data belongs to the server. Then you have data that exists only in the browser, like whether a sidebar is open, the current theme, or a multi-step form’s progress. Mixing these two types of state in the same place is a common source of bugs.
This is where a clear separation saves the day. Instead of one giant tool trying to do everything, we use two specialized tools. Zustand is for client state—the things that happen purely on the user’s machine. React Query is for server state—the data we fetch, cache, and update from a backend. When you stop forcing your server cache into your UI state manager, everything gets simpler.
What does client state look like in practice? Imagine a dashboard with collapsible panels, a dark mode toggle, and a complex filter widget for a data table. This is Zustand’s territory. It gives you a minimal, straightforward way to create a store. You write a function that defines your state and actions. The result is a hook you can use anywhere in your app.
Here’s a small example. Let’s create a store for UI preferences.
import { create } from 'zustand';
const useUiStore = create((set) => ({
isSidebarOpen: true,
theme: 'light',
toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
setTheme: (newTheme) => set({ theme: newTheme }),
}));
// Using it in a component
function Header() {
const { isSidebarOpen, toggleSidebar } = useUiStore();
return <button onClick={toggleSidebar}>{isSidebarOpen ? 'Close' : 'Open'}</button>;
}
It’s just a custom hook. There’s no provider to wrap your app in, no complex boilerplate. You call useUiStore() and get exactly the state and functions you need. This simplicity is its greatest strength for managing local interactions.
Now, what about the data from your database? This is where React Query changes the game. It assumes that server data is a cache that needs careful management. Have you ever struggled with knowing when to refetch data after a user submits a form? React Query handles that for you.
You tell it how to fetch data for a specific “query key.” It handles caching, background updates, and error states. When you perform a mutation, like posting a new item, you can tell React Query to update the relevant cache. This keeps your UI in sync with the server without manual refetching logic.
Look at how clean data fetching becomes.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function ProductList() {
const queryClient = useQueryClient();
// Fetching and caching products
const { data: products, isLoading } = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(res => res.json())
});
// Adding a new product
const mutation = useMutation({
mutationFn: (newProduct) => fetch('/api/products', {
method: 'POST',
body: JSON.stringify(newProduct)
}).then(res => res.json()),
onSuccess: () => {
// Invalidate the cache, triggering a fresh fetch
queryClient.invalidateQueries({ queryKey: ['products'] });
}
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
{products.map(p => <div key={p.id}>{p.name}</div>)}
<button onClick={() => mutation.mutate({ name: 'New Item' })}>
Add Product
</button>
</div>
);
}
See the pattern? React Query manages the server’s truth. Your components don’t store API results in state; they subscribe to a query cache. This eliminates entire classes of bugs related to stale data.
So how do they work together? Beautifully. They occupy different layers. Your Zustand store might hold a searchTerm string for filtering. Your React Query hook fetches the full list of products. You combine them locally in the component.
function ProductDashboard() {
// Client state: the user's search input
const searchTerm = useUiStore((state) => state.searchTerm);
// Server state: the full product list
const { data: allProducts } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts
});
// Derived local state: filtering the cached list
const filteredProducts = allProducts?.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
) || [];
return (
<>
<SearchInput />
<ProductGrid products={filteredProducts} />
</>
);
}
Zustand holds the volatile, client-specific data. React Query holds the authoritative server data. The component is the meeting point, combining them for presentation. This separation is the key to scalability. New developers on the team can instantly understand what type of state they are dealing with and which tool to use.
What happens when you need to share server data with your client state? You pass it as a simple function argument. For example, a Zustand action that updates a user preference might need the current user ID from React Query’s cache. You would read the ID from the query cache and pass it to the Zustand action. The libraries remain independent, communicating through your application code.
This approach also makes testing much easier. You can test your Zustand stores in isolation, mocking any functions. You can test your components using React Query’s testing utilities to simulate different server states. The boundaries are clear.
For me, adopting this pattern was a turning point. It stopped the endless debates about “where to put state.” The rule became simple: Is this from a server? Use React Query. Is this for the UI? Use Zustand. This mental model reduces decision fatigue and lets you focus on building features.
The combination is lean. You avoid the overhead of a heavyweight framework. You get incredible developer experience with full TypeScript support. Your app’s performance improves because React Query’s intelligent caching prevents unnecessary network requests. Your code becomes more maintainable because the responsibilities are clearly divided.
Have you tried managing server cache with a global client store before? How did you handle refetching on window focus or reconnection? React Query handles these edge cases out of the box, allowing you to delete hundreds of lines of fragile logic.
I encourage you to try this setup in your next project. Start by moving all your fetch calls into React Query hooks. Then, identify the UI state that’s cluttering your components and lift it into a small Zustand store. You’ll likely be surprised by how much simpler your components become.
If this approach to structuring your state makes sense, I’d love to hear about your experience. Did it help clarify your code? What challenges did you face? Please share your thoughts in the comments below—discussing these patterns helps everyone learn. If you found this useful, consider liking and sharing it with other developers who might be wrestling with their state management strategy. Let’s build more understandable 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