js

Zustand and React Query Together: A Smarter React State Management Pattern

Learn when to use Zustand for client state and React Query for server state to build cleaner React apps. Discover the pattern now.

Zustand and React Query Together: A Smarter React State Management Pattern

I was recently building a dashboard for a complex application. My components were a tangle of loading spinners, error messages, and local UI toggles, all fighting for attention with the core data from our API. It felt messy. I realized my state management approach was the problem. I was trying to use one tool for two very different jobs: data from a server and the application’s own interactive state. This struggle is what pushed me to explore a clearer path: using Zustand and React Query together.

Why do we need two libraries? Think about the data in your app. Some of it, like a user’s profile or a list of orders, comes from a server. It’s shared, can become outdated, and needs synchronization. Other data, like whether a sidebar is open or the current value in a form field, exists only in the browser. Mixing these causes confusion. Server state belongs to React Query. Client state belongs to Zustand.

Let’s start with React Query. It sees your server as the single source of truth. You tell it how to fetch data, and it handles everything else: caching, refetching in the background, and managing loading states. You don’t store this data in React state or a global store. You let React Query manage it.

import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json())
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <h1>Hello, {data.name}</h1>;
}

See how clean that is? The data, isLoading, and error come directly from the query. If another component needs the same user, React Query will serve the cached data instantly. It removes the need for manual useEffect calls and prop drilling for server data.

So, where does Zustand fit in? It’s for everything else. That open/closed state of a modal? The current theme (light/dark)? The selected filters on a table that haven’t been sent to the server yet? This is client state. Zustand gives you a simple, minimal store.

import { create } from 'zustand';

const useThemeStore = create((set) => ({
  theme: 'light',
  toggleTheme: () => set((state) => ({ 
    theme: state.theme === 'light' ? 'dark' : 'light' 
  })),
}));

// In a component
function ThemeToggle() {
  const { theme, toggleTheme } = useThemeStore();
  return (
    <button onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  );
}

The beauty is in their separation. Your components become declarative. They ask React Query for server state and ask Zustand for client state. There’s no ambiguity. But what about when they need to talk to each other? This is where the integration shines.

Imagine a search component. The search term itself is client state—it’s just text in an input box. But the results come from the server. We can use Zustand to hold the search term and React Query to fetch based on it.

import { create } from 'zustand';
import { useQuery } from '@tanstack/react-query';

// Zustand store for the client state (search term)
const useSearchStore = create((set) => ({
  searchTerm: '',
  setSearchTerm: (term) => set({ searchTerm: term }),
}));

function SearchResults() {
  // Get client state from Zustand
  const { searchTerm } = useSearchStore();
  
  // Use it to drive a server query with React Query
  const { data, isLoading } = useQuery({
    queryKey: ['search', searchTerm], // Query key depends on Zustand state!
    queryFn: () => fetch(`/api/search?q=${searchTerm}`).then(res => res.json()),
    enabled: !!searchTerm, // Only run the query if we have a term
  });

  // ... render results
}

Notice the flow? The searchTerm lives in Zustand. When it changes, it causes the SearchResults component to re-render. This triggers React Query to see a new queryKey ([‘search’, ‘newTerm’]) and fetch new data. The concerns are separate, but the integration is seamless.

Have you ever cached server data in your global store, only to find it stale and out of sync? This pattern prevents that. React Query is the expert on server data freshness. Zustand is the expert on local UI state. By letting each do its specialized job, you avoid these common pitfalls.

What does this mean for a large application? It provides a shared rulebook for your team. New developers can look at a piece of state and immediately know where to find it and how it’s managed. Is it from an API? Check the React Query hooks. Is it a UI control? Check the Zustand stores. This clarity is priceless as projects grow.

The result is an application that feels solid. Data loading and caching are handled robustly in the background by React Query. The interactive parts of your UI remain snappy and simple with Zustand. You stop writing repetitive code for fetching and caching, and you start focusing on features.

I encourage you to try this split in your next project. Start by asking for every piece of state: “Did this come from a server?” If yes, it goes to React Query. If no, it’s a candidate for Zustand. You might be surprised how much cleaner your components become.

What challenges have you faced when managing different types of state in React? Have you found other ways to create this separation? I’d love to hear about your experiences in the comments below. If this approach to structuring your state makes sense, please share this article with other developers who might be facing similar challenges.


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

Keywords: Zustand, React Query, React state management, client state, server state



Similar Posts
Blog Image
How to Integrate Prisma with GraphQL for Type-Safe Database Operations and Modern APIs

Learn how to integrate Prisma with GraphQL for type-safe, efficient APIs. Master database operations, resolvers, and build modern full-stack applications seamlessly.

Blog Image
Build Scalable Event-Driven Architecture with NestJS, Redis, MongoDB: Complete Professional Guide 2024

Learn to build scalable event-driven architecture with NestJS, Redis & MongoDB. Includes event sourcing, publishers, handlers & production tips. Start building today!

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Full-Stack Development

Learn to integrate Next.js with Prisma ORM for powerful full-stack development. Build type-safe web apps with seamless database management and optimal performance.

Blog Image
How to Generate Pixel-Perfect PDFs and Scrape Dynamic Sites with Puppeteer and NestJS

Learn how to use Puppeteer with NestJS to create high-fidelity PDFs and scrape dynamic web content with ease.

Blog Image
Build a Real-Time Analytics Dashboard with Fastify, Redis Streams, and WebSockets Tutorial

Build real-time analytics with Fastify, Redis Streams & WebSockets. Learn data streaming, aggregation, and production deployment. Master high-performance dashboards now!

Blog Image
Complete Guide to Next.js Prisma Integration: Full-Stack Database Management Made Simple

Learn how to integrate Next.js with Prisma for powerful full-stack database management. Build type-safe applications with seamless data operations and modern ORM features.