Using Zustand for Global Client-Side State & TanStack Query for Server-Side States

Introduction

State management is one of the most important concepts in building modern React applications. But not all state is the same. Some lives in the client (like toggling a dark mode switch), while other state must come from a server (like fetching a list of users).

In this blog, we’ll explore:

  • Client-side state with Zustand

  • Server-side state with TanStack Query

  • ✅ How to combine them for scalable apps

Think of it like two brains working together — one handling quick local actions, and the other syncing data with the outside world.


🌐 Client State vs Server State

Before diving into code, let’s clarify:

AspectClient StateServer State
Where it livesInside the app (memory/store)On backend/database
ExamplesTheme, modals, cart countUser list, product data
Sync needed?No (app-only)Yes (needs backend sync)
Best ToolZustand, Redux, ContextTanStack Query, SWR

👉 Rule of thumb: Use Zustand for UI-driven, temporary state. Use TanStack Query for remote, async data.


🐻 Zustand for Global Client-Side State

Zustand (German for “state”) is a lightweight but powerful state manager. Unlike Redux, it avoids boilerplate and lets you build simple stores with just a few lines of code.

🔹 Creating a Store

import { create } from 'zustand'

const useThemeStore = create((set) => ({
  darkMode: false,
  toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode }))
}))

export default useThemeStore

🔹 Using the Store in a Component

import useThemeStore from './store'

function Navbar() {
  const { darkMode, toggleDarkMode } = useThemeStore()

  return (
    <nav className={darkMode ? "bg-black" : "bg-white"}>
      <button onClick={toggleDarkMode}>
        Toggle Dark Mode
      </button>
    </nav>
  )
}

💡 That’s it! No reducers, no actions, no context providers — just a store and a hook.


⚡ TanStack Query for Server-Side State

While Zustand shines locally, TanStack Query (React Query) is the gold standard for managing async server state. It automatically handles caching, refetching, background updates, and even optimistic UI updates.

🔹 Fetching Data

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

function Users() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => res.json())
  })

  if (isLoading) return <p>Loading...</p>
  if (error) return <p>Error fetching users</p>

  return (
    <ul>
      {data.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  )
}

🔹 Optimistic Updates (Mutation Example)

import { useMutation, useQueryClient } from '@tanstack/react-query'

function AddUser() {
  const queryClient = useQueryClient()

  const mutation = useMutation({
    mutationFn: (newUser) => 
      fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(newUser),
      }),
    onMutate: async (newUser) => {
      // Optimistic update
      await queryClient.cancelQueries(['users'])
      const previousUsers = queryClient.getQueryData(['users'])
      queryClient.setQueryData(['users'], old => [...old, newUser])
      return { previousUsers }
    },
    onError: (err, newUser, context) => {
      queryClient.setQueryData(['users'], context.previousUsers)
    },
    onSettled: () => {
      queryClient.invalidateQueries(['users'])
    }
  })

  return (
    <button onClick={() => mutation.mutate({ id: Date.now(), name: 'New User' })}>
      Add User
    </button>
  )
}

⚡ This way, the UI updates instantly while the server request is still happening — and gracefully rolls back on errors.


🔗 Zustand + TanStack Query: A Perfect Pair

In real-world apps, you’ll often need both client + server states. For example:

  • Zustand manages a sidebar toggle or selected filters

  • TanStack Query fetches and caches products based on those filters

🔹 Architecture Diagram Idea

[ UI Components ]
       |
  Zustand Store (UI State)
       |
  TanStack Query (Server State)
       |
   Backend / API

This separation of concerns makes apps scalable, predictable, and performant.


✅ Conclusion

  • Use Zustand for global client-side state (toggles, theme, UI states).

  • Use TanStack Query for server-side data (fetching, caching, mutations).

  • Combine both for a robust, modern state management stack.

By mastering both tools, you’ll simplify your state management and make your React apps faster and more reliable.

0
Subscribe to my newsletter

Read articles from prashant chouhan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

prashant chouhan
prashant chouhan