I’ve been thinking about state management lately—not as an abstract concept, but as a daily reality for building modern React apps. If you’ve ever found yourself wrestling with API data that seems to vanish, or mixing up user preferences with server responses, you’re not alone. That’s exactly why I want to talk about a specific pairing: Zustand and React Query. It’s not about using one tool to rule them all, but about letting each one do what it does best.
So often, we try to force a single solution onto two very different problems. Client state—things like whether a sidebar is open, a selected theme, or a form’s draft values—lives and dies in the browser. Server state—your user profiles, product listings, or dashboard metrics—comes from an external source, can be shared across users, and needs careful synchronization. Merging them in one place often leads to a tangled mess.
This is where a clean separation makes all the difference. I use Zustand for what happens right here in the UI. It’s incredibly simple. You create a store with a small, focused piece of state and the functions to update it. There’s no bulky setup. Let me show you what I mean.
// store/themeStore.js
import { create } from 'zustand';
const useThemeStore = create((set) => ({
mode: 'light',
toggleMode: () => set((state) => ({
mode: state.mode === 'light' ? 'dark' : 'light'
})),
}));
// Component.jsx
function ThemeToggle() {
const { mode, toggleMode } = useThemeStore();
return <button onClick={toggleMode}>Current: {mode}</button>;
}
See how straightforward that is? Zustand gives you a minimal API to manage transient, local state without any fuss. But what happens when you need data that isn’t local? What do you do when you need to ask your backend for the latest information, show a loading spinner, handle an error, or cache the response to avoid asking again? This is a completely different challenge.
Have you ever stored API data in a global client store, only to find it becomes stale or out of sync? I have. That’s the moment you realize you need a dedicated tool for server communication. This is where React Query enters the picture. It treats server state as a separate concern, providing caching, background updates, and pagination without you writing that logic from scratch.
Think of React Query as a smart assistant for your data fetching. You tell it where to get the data, and it handles the rest: loading states, errors, caching, and even updating the cache when needed. Here’s a basic example.
// hooks/useUserData.js
import { useQuery } from '@tanstack/react-query';
const fetchUser = async (userId) => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
};
export function useUserData(userId) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // Data is fresh for 5 minutes
});
}
// UserProfile.jsx
function UserProfile({ id }) {
const { data: user, isLoading, error } = useUserData(id);
if (isLoading) return <p>Loading profile...</p>;
if (error) return <p>Error: {error.message}</p>;
return <h1>Hello, {user.name}</h1>;
}
Notice how React Query manages the entire lifecycle of that server request. You don’t store the result in a Zustand store. You don’t have to write logic to show loaders. It’s all handled. But now, a question might arise: how do these two libraries actually work together in a real component? Where does one stop and the other start?
The key is in their clear roles. In a typical app feature, you might use both side-by-side. Zustand holds the UI’s immediate state, while React Query manages the data from your API. They are neighbors, not competitors. Let’s look at a more combined scenario, like a product page with a shopping cart.
// store/cartStore.js
import { create } from 'zustand';
const useCartStore = create((set) => ({
items: [],
addItem: (product) =>
set((state) => ({ items: [...state.items, product] })),
}));
// ProductPage.jsx
import { useProduct } from '../hooks/useProduct'; // Uses React Query
import useCartStore from '../store/cartStore';
function ProductPage({ productId }) {
// Server state from React Query
const { data: product, isLoading } = useProduct(productId);
// Client state from Zustand
const addItem = useCartStore((state) => state.addItem);
if (isLoading) return <p>Loading product details...</p>;
return (
<div>
<h2>{product.name}</h2>
<p>{product.description}</p>
<button onClick={() => addItem(product)}>
Add to Cart
</button>
</div>
);
}
In this example, the boundary is clear. The product data, including its name and description, is fetched and cached by React Query. The action of adding that product to the items array is managed by Zustand, because the cart is a client-side concept. This separation is powerful. It makes your code easier to reason about, test, and maintain as your application grows.
Why does this matter for a larger application? Because complexity grows in the connections between things. When server state and client state are mixed, a change in one can have unexpected effects on the other. By keeping them separate, you reduce this surface area for bugs. Your API cache updates don’t accidentally reset a UI toggle. Your form state doesn’t get cleared by a background data refetch.
So, is this approach the right fit for every project? Not necessarily. For very simple apps, useState and useEffect might be enough. But the moment you have multiple components needing the same server data, or complex UI state that’s used across different pages, this combination provides a scalable structure without heavy overhead.
Adopting this pattern encourages better habits. You start asking, “Is this state from the server, or is it local?” That simple question leads to cleaner architecture. You stop overloading your client stores with API responses and avoid the inevitable bugs that come from manually syncing cached data.
I encourage you to try this setup in your next project. Start by using React Query for any data fetching. Use Zustand for the UI controls and preferences that are unique to this user’s session. Feel the clarity that comes from having each piece of state in its rightful home.
What has your experience been with managing these two types of state? Have you tried other combinations? I’d love to hear what’s worked for you. If you found this perspective helpful, please share it with your team or fellow developers. Let’s continue the conversation in the comments below.
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