Zustand + React Query: The Lightweight Duo for State Management

Chamith Madusanka
5 min read2 days ago

--

Use React Query for server state and Zustand for UI & client state.

Introduction

State management in React applications has always been a hot topic. Developers often find themselves choosing between Redux, Context API, and other state management libraries. However, in recent years, a new lightweight and intuitive solution has been gaining traction: Zustand.

When combined with React Query, Zustand offers a powerful yet simple way to handle both client-side and server-side state efficiently. In this article, we’ll explore why Zustand is becoming a go-to choice for state management, how it complements React Query, and why this duo is a trend worth adopting.

Why Zustand?

Zustand is a minimalistic state management library that eliminates the need for complex boilerplate. Here’s why developers love it:

  • Lightweight & Fast — Zustand is just a few kilobytes in size.
  • No Boilerplate — Unlike Redux, it doesn’t require reducers or actions.
  • Easy to Use — Simple API with a useStore hook for managing state.
  • Optimized Re-Renders — Selectors prevent unnecessary re-renders, improving performance.
  • Supports Async Actions — Directly integrates with async operations.

Example of a Zustand store:

import { create } from "zustand";
import { Post } from "../types";

interface Post {
id: number;
title: string;
content: string;
}

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);

Zustand Best Practices 🚀

1️⃣ Only Export Custom Hooks

• Don’t export the entire store; instead, expose only specific selectors via custom hooks.
• This ensures components only subscribe to necessary state changes, improving performance.

import { create } from "zustand";

interface PostStore {
selectedPostId: number | null;
setSelectedPostId: (id: number | null) => void;
}

const usePostStore = create<PostStore>((set) => ({
selectedPostId: null,
setSelectedPostId: (id) => set({ selectedPostId: id }),
}));

// ✅ Export custom selector hooks
export const useSelectedPostId = () => usePostStore((state) => state.selectedPostId);
export const usePostActions = () => usePostStore((state) => ({ setSelectedPostId: state.setSelectedPostId }));

2️⃣ Prefer Atomic Selectors

Why?
• Ensures minimal re-renders.
• Prevents unnecessary state updates.

🚨 Bad: (Causes Unnecessary Re-renders) — Every time any part of the store updates, the component re-renders.

const { selectedPostId, setSelectedPostId } = usePostStore();

Good: (Optimized Atomic Selectors)

export const useSelectedPostId = () => usePostStore((state) => state.selectedPostId);
export const usePostActions = () => usePostStore((state) => ({ setSelectedPostId: state.setSelectedPostId }));

3️⃣ Separate Actions from State

• Actions never change, so store them in a separate namespace.
• This prevents unnecessary re-renders when accessing actions.

const usePostStore = create<PostStore>((set) => ({
selectedPostId: null,
actions: {
setSelectedPostId: (id: number | null) => set({ selectedPostId: id }),
},
}));

export const useSelectedPostId = () => usePostStore((state) => state.selectedPostId);
export const usePostActions = () => usePostStore((state) => state.actions);

💡 Now, we can use actions without re-renders:

const { setSelectedPostId } = usePostActions();

4️⃣ Model Actions as Events, Not Setters

Why?
• Keeps logic inside the store, not in components.

🚨 Bad: (Logic inside component)

const { setSelectedPostId } = usePostActions();
setSelectedPostId(1);

Good: (Encapsulated logic in store)

const usePostStore = create<PostStore>((set) => ({
selectedPostId: null,
actions: {
selectPost: (id: number | null) => set({ selectedPostId: id }),
},
}));

export const usePostActions = () => usePostStore((state) => state.actions);

// Component usage
const { selectPost } = usePostActions();
selectPost(1);

5️⃣ Keep Store Scope Small, Zustands lets you create multiple small stores instead of one big store.

The Power of Zustand & React Query 🎯

While Zustand is great for managing local state, React Query excels at handling server-state, API calls, caching, and background fetching. Instead of using Zustand for fetching and storing remote data, React Query takes care of the network layer, and Zustand handles UI state and local state logic.

Zustand

🛠 Minimal API — Define state & actions in a single function

🚀 Zero Boilerplate — No reducers, no context wrapper needed

🔄 Reactive Store — Automatic updates without extra setup

🔗 Global & Local State — Manage both effortlessly

📦 TypeScript Support — Strong typing for better DX

🧩 Middleware Ready — Persist state, log actions, and more

React Query 🌍

📡 Data Fetching & Caching — Fetch once, reuse everywhere

🚀 Automatic Background Sync — Always fresh data

🔄 Refetching & Pagination — Built-in data refetch strategies

Optimistic Updates — Snappy UI with rollback support

📦 Normalized Cache — Efficient state updates

🔗 Seamless Zustand Integration — Store query results globally

Benefits of Using Both Together:

  1. Separation of Concerns — React Query manages server state while Zustand handles UI-related state.
  2. Better Performance — React Query reduces unnecessary network requests, while Zustand optimizes re-renders.
  3. Simpler API Management — No need for complex Redux boilerplate or Context API.

Integrating React Query with Zustand

Here’s how you can use React Query with Zustand to store user data efficiently:

import { useQuery } from "@tanstack/react-query";
import { fetchPosts } from "../api/posts";
import { usePostActions } from "../store/usePostStore";
import { Post } from "../types";

const PostsList = () => {
const {
data: posts,
isLoading,
error,
} = useQuery<Post[]>({
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",
border: "1px solid #ddd",
padding: "10px",
margin: "5px",
borderRadius: 10,
}}
>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
</div>
);
};

export default PostsList;

In this example, React Query fetches posts data from an API, while Zustand manages the locally stored post state.

Developers Are Switching to Zustand & React Query

The React ecosystem is moving toward lighter, more efficient state management solutions. Zustand has gained popularity because it removes the complexity of traditional solutions like Redux while keeping the power of a global store. Similarly, React Query is now the preferred tool for handling API requests and caching, replacing solutions like Redux-Saga and RTK Query.

Key Trends:

  • Less Boilerplate, More Productivity — Developers are choosing Zustand over Redux due to its simpler API and smaller learning curve.
  • Optimized API HandlingReact Query is the de facto choice for handling asynchronous data in React applications.
  • Better Performance — Zustand and React Query optimize state updates, reducing unnecessary re-renders.

Final Thoughts

If you’re looking for a lightweight, scalable, and easy-to-use solution for state management in React, Zustand and React Query are an excellent combination. Zustand’s minimalistic global store, combined with React Query’s efficient server-state management, provides a clean and performant architecture.

So, if you’re still using Redux or struggling with Context API, now might be the time to give Zustand and React Query a try!

You can find the complete source code for this project on https://github.com/chambits/zustand-react-query.

--

--

Chamith Madusanka
Chamith Madusanka

Written by Chamith Madusanka

Full Stack Enthusiast | JavaScript | TypeScript | ReactJs | NextJs | | NodeJs | NestJs | Serverless | Find me on Linkedin https://www.linkedin.com/in/chamith24/

No responses yet