Blenra LogoBlenra
Web Components

Architecting Zustand and React Query Together Using AI

By Naveen Teja Palle6 min read
Architecting Zustand and React Query Together Using AI

One of the most common architectural mistakes in modern React applications is treating all state as the same type of data. Zustand and React Query (TanStack Query) solve fundamentally different problems, and mixing them incorrectly leads to cache invalidation bugs, stale data issues, and unnecessary API calls.

The rule is simple: React Query owns all server state (data that lives on your backend, has loading/error states, and can be out of sync with the UI). Zustand owns all client state (UI state, user preferences, filters, modal open/close — anything that doesn't need to be fetched from a server).

In this guide, we use engineered AI prompts to generate the exact architecture patterns that correctly integrate these two libraries in a production Next.js or React application.

The Core Mental Model: Server State vs. Client State

State TypeZustandReact Query
Modal open/close✅ Yes❌ No
Sidebar expanded✅ Yes❌ No
Active tab / filter✅ Yes❌ No
User auth token (in-memory)✅ Yes❌ No
API data (users, products)❌ No✅ Yes
Loading / error states❌ No✅ Yes (automatic)
Paginated data❌ No✅ Yes (useInfiniteQuery)
Cached API responses❌ No✅ Yes (automatic)

Prompt 1: Clean Architecture Setup

Use this prompt to generate the foundational folder structure and store setup for a scalable React app:

"Act as a Senior React Architect. Design a TypeScript folder structure and code pattern for a Next.js 14 app that uses Zustand for client state and TanStack Query v5 for server state. Generate: (1) A zustand store file (store/useUIStore.ts) managing: sidebar open/close, active theme, notification count, and modal states. Use typed slices and the immer middleware. (2) A React Query configuration file (lib/queryClient.ts) with: staleTime of 5 minutes, gcTime of 10 minutes, retry: 1, and a global onError handler that shows a toast notification. (3) A custom hook (hooks/useUsers.ts) that uses useQuery to fetch a paginated user list from '/api/users'. Include TypeScript interfaces for User and ApiResponse<T>. (4) Show how the UI store and query results are combined in a UserDashboard component without the two libraries interfering with each other."

Prompt 2: Optimistic Updates Pattern

Optimistic updates are where the Zustand + React Query combination becomes most powerful. This pattern immediately reflects a change in the UI before the server confirms it:

"Write a React Query optimistic update mutation in TypeScript for a 'like' feature. When the user clicks like: (1) Immediately update the UI using queryClient.setQueryData to toggle the isLiked boolean and increment likeCount, WITHOUT waiting for the server. (2) Call the API using useMutation's mutationFn. (3) On error, use the onError callback to roll back the optimistic update using the 'context' returned from onMutate. (4) On success, call queryClient.invalidateQueries to refetch the canonical data from the server. Store the user's 'pendingLikes' Set in a Zustand store to prevent double-clicking. Use full TypeScript generics."

Prompt 3: Zustand-Driven Query Filters

A common pattern is using Zustand to manage filter/sort/search state that drives React Query fetches. Here's the cleanest way to wire this up:

"Generate a TypeScript React pattern where: (1) A Zustand store (useSearchStore) manages: searchQuery (string), sortBy ('name' | 'date' | 'price'), filterStatus ('all' | 'active' | 'archived'), and currentPage (number). (2) A useProducts() custom hook uses TanStack Query's useQuery with a queryKey that includes all Zustand filter values as dependencies, so the query automatically refetches when any filter changes. (3) The queryKey format must be: ['products', { searchQuery, sortBy, filterStatus, currentPage }]. (4) Include a resetFilters action in the Zustand store that calls queryClient.invalidateQueries(['products']) after resetting. Show the full TypeScript code for both files."

Prompt 4: Persistent Client State with Zustand

Some client state (like user preferences) should persist across page refreshes. Zustand's persist middleware enables this without any extra libraries:

"Write a Zustand store in TypeScript using the persist middleware to save user preferences to localStorage. The store should manage: theme ('dark' | 'light' | 'system'), language ('en' | 'es' | 'fr' | 'de'), dashboardLayout ('grid' | 'list'), notificationsEnabled (boolean), and lastVisitedRoute (string). Requirements: (1) Use the persist middleware with a custom storage name 'blenra-preferences'. (2) Implement a partial persist using the partialize option to exclude lastVisitedRoute from localStorage. (3) Add a resetPreferences action. (4) Add a hasHydrated boolean to handle Next.js SSR hydration mismatch (don't render theme-dependent UI until store has hydrated from localStorage). Use TypeScript throughout."

Pro Tips

⚠️ Never Put Server Data in Zustand

The most common mistake is duplicating React Query cache data into Zustand. This creates two sources of truth that can diverge. If a React Query mutation updates the server, your Zustand copy won't know about it. Let React Query own server state exclusively.

💡 Use Zustand as a QueryKey Factory

Instead of manually passing filter state to query keys, create a Zustand selector that returns the query key array. This ensures your query keys are always in sync with your filter state, making cache invalidation predictable and explicit.

Frequently Asked Questions

Q: Should I use Zustand or Context API for small apps?

A: For very small apps (1–3 components sharing state), React Context is fine. Once you have more than 3 components sharing state, or state updates causing performance issues (Context re-renders all consumers on every update), switch to Zustand. Zustand uses subscription-based updates, so only components that subscribe to the specific state slice re-render.

Q: How do I share Zustand store state with React Query mutations?

A: Access the Zustand store inside a custom hook that wraps useMutation. You can call const { user } = useAuthStore() alongside const mutation = useMutation({...}) in the same hook. The Zustand state is just a regular function call — it works anywhere in React's render context.

Q: Is TanStack Query v5 compatible with Next.js 14 App Router?

A: Yes, but you need to set up a QueryClientProvider at the top of your component tree in a Client Component wrapper. Create a file like providers/QueryProvider.tsx with "use client" that wraps children with <QueryClientProvider client={queryClient}>. Then wrap your layout's children with <QueryProvider>.

NP

Naveen Teja Palle

Frontend Architect · State Management Specialist

React engineer specializing in scalable state management architectures for high-traffic SaaS applications. Has shipped Zustand + React Query integrations across multiple production platforms.

1,000+ React State Management Prompts

Zustand, React Query, Redux Toolkit, Jotai — every modern React state pattern, production-ready.

Explore React Prompts →