Zustand + React Query: Modern State Management in React


Never miss an update
Subscribe to receive news and special offers.
By subscribing you agree to our Privacy Policy.
Use React Query for server state and Zustand for UI & client state.
State management in React applications has evolved. While Redux and Context API have dominated the past, modern developers are moving toward lighter, simpler, and more efficient tools. Zustand, a minimalistic state manager, and React Query, a powerful data-fetching library, make a perfect combo.
In this guide, you'll learn why this pair is gaining popularity, best practices for using Zustand, and how to seamlessly combine both for a scalable, maintainable architecture.
Zustand is a small but mighty state manager with no boilerplate, excellent performance, and TypeScript support.
Key benefits:
useStore hook.Example Zustand store:
import { create } from "zustand";
import { Post } from "../types";
interface PostStore {
selectedPost: Post | null;
actions: {
setSelectedPost: (post: Post) => void;
};
}
const usePostStore = create<PostStore>((set) => ({
selectedPost: null,
actions: {
setSelectedPost: (post) => set({ selectedPost: post }),
},
}));
export const useSelectedPost = () => usePostStore((state) => state.selectedPost);
export const usePostActions = () => usePostStore((state) => state.actions);Only expose selectors and actions via hooks — not the entire store.
// ✅ Good: selector hook
export const useSelectedPostId = () => usePostStore((state) => state.selectedPostId);Avoid pulling multiple values from the store to reduce re-renders.
// ❌ Bad: causes re-renders on any state change
const { selectedPostId, setSelectedPostId } = usePostStore();
// ✅ Good: atomic selectors
export const useSelectedPostId = () => usePostStore((state) => state.selectedPostId);Group actions under an actions object. This ensures they remain referentially stable and don't cause rerenders.
const usePostStore = create<PostStore>((set) => ({
selectedPostId: null,
actions: {
setSelectedPostId: (id) => set({ selectedPostId: id }),
},
}));Encapsulate logic inside your store, not your components.
// ✅ Better: logic inside store
const usePostStore = create<PostStore>((set) => ({
selectedPostId: null,
actions: {
selectPost: (id) => set({ selectedPostId: id }),
},
}));Create multiple small stores instead of one large global store for better maintainability.
Zustand excels at managing UI and client state. React Query handles server state like API data, background updates, and caching. Together, they solve different problems — without stepping on each other.
import { useQuery } from "@tanstack/react-query";
import { fetchPosts } from "../api/posts";
import { usePostActions } from "../store/usePostStore";
const PostsList = () => {
const { data: posts, isLoading, error } = useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
});
const { setSelectedPost } = usePostActions();
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error loading posts</p>;
return (
<div>
<h2>Posts</h2>
{posts?.map((post) => (
<div
key={post.id}
onClick={() => setSelectedPost(post)}
style={{ cursor: "pointer", padding: "10px", border: "1px solid #ccc" }}
>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
</div>
);
};💡 Why not use React Query for selected post? Because that’s UI state, not server state. Keep them separate.
Storing posts in an array means selecting a post by ID requires an O(n) search. At scale, that’s expensive.
Instead, normalize your data into an object:
{
1: { id: 1, title: "..." },
2: { id: 2, title: "..." }
}Now lookups are O(1).
Zustand and React Query are now the default stack for many teams building fast, scalable apps.
Zustand and React Query solve different, complementary problems — and they do it well. Zustand handles UI logic, React Query handles async data. If you're still using Redux or stuck with Context spaghetti, it's time to modernize.
Stay tuned for our next guide on data normalization with Redux Toolkit and Zustand!