Learning Zustand - Server State & Data Integration
Episode 12 of 23

Learning Zustand - Server State & Data Integration

This episode distinguishes server state from client state, then establishes rules for when API data goes into Zustand and when into TanStack Query or SWR. You also learn the hybrid pattern that separates the server cache from the global store, with an auth and user profile example.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

A modern application doesn't just keep UI state in memory — it also talks to APIs to fetch user data, product lists, and more. A frequently asked question: does server data go into Zustand or into a cache library like TanStack Query? Episode 12 answers that question by distinguishing two different categories of state and laying out the hybrid pattern used by production teams.

You'll understand the difference between server state and client state, the criteria for when API data goes into Zustand, when to use TanStack Query or SWR, and then build an auth and user profile example with the hybrid pattern.

Server State vs Client State

Server state is data owned by the server and synchronized through APIs: user profiles, lists of posts, account balances. Client state is data born and lived in the browser: modal status, form values, the active theme, or the current page.

This distinction matters because it determines where the data lives. Server state needs caching, retry, and invalidation when the data changes on the server. Client state needs speed and doesn't need to sync with the server. Mixing both in a single store makes the code hard to predict: the store ends up handling loading, error, caching, and refetching all at once.

When API Data Goes into Zustand

There are several situations where keeping API data in Zustand makes sense:

  • The data is read by many components and rarely changes during a session.
  • The data is used for global actions like authentication.
  • The data needs to be modified locally before being sent back to the server.

The most common example is the session token and user profile. The token must be available synchronously for every request and shouldn't change unpredictably:

JSAuth session in Zustand
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
 
interface AuthState {
  token: string | null
  user: User | null
  setSession: (token: string, user: User) => void
  clearSession: () => void
}
 
export const useAuth = create<AuthState>()(
  persist(
    (set) => ({
      token: null,
      user: null,
      setSession: (token, user) => set({ token, user }),
      clearSession: () => set({ token: null, user: null }),
    }),
    { name: 'auth-session' },
  ),
)

setSession(token, user) stores session data once per login, and any component can read the token synchronously without waiting for a new API response.

When to Use TanStack Query or SWR

When data changes often on the server and is read many times over, use a server cache library. TanStack Query and SWR provide caching, request deduplication, retry, refetch on window focus, and query invalidation — features Zustand doesn't have.

Install TanStack Query in your project:

Install TanStack Query
npm i @tanstack/react-query

Then define a query with a queryKey and queryFn:

JSUser profile query
import { useQuery } from '@tanstack/react-query'
 
export function useUserProfile(userId: string) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () =>
      fetch(`/api/users/${userId}`).then((res) => res.json()),
    staleTime: 60_000,
  })
}

queryKey: ['user', userId] creates a cache per user, and a staleTime of 60 seconds prevents excessive refetches.

Hybrid Pattern: Zustand and TanStack Query

The healthiest combination: Zustand stores global client state like tokens and preferences, TanStack Query stores the server cache. When the user changes, invalidate queries in TanStack Query and update the token in Zustand:

JSInvalidating queries after login
async function login(email: string, password: string) {
  const res = await fetch('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  })
  const data = await res.json()
  useAuth.getState().setSession(data.token, data.user)
  await queryClient.invalidateQueries({ queryKey: ['user'] })
}

useAuth.getState().setSession(...) updates the global store, while invalidateQueries forces TanStack Query to refetch the related profile. With this pattern, the store never holds a server cache, and the server cache never handles global actions.

Closing

Episode 12 separates two worlds of state: server state that lives in TanStack Query or SWR, and client state that lives in Zustand. Session and UI data go into Zustand; data that changes on the server and is read many times goes into a query cache with invalidation.

Key takeaways:

  • Server state needs caching, retry, and invalidation; client state needs speed.
  • Tokens and auth sessions suit Zustand so they're available synchronously.
  • TanStack Query and SWR handle API data caching with queryKey and staleTime.
  • The hybrid pattern separates the server cache from the global store.
  • useAuth.getState().setSession updates the store from outside hooks.
  • invalidateQueries syncs the query cache after a login or logout action.

In the next episode we will discuss performance and transient updates — controlling re-renders with selectors and useShallow, avoiding new objects on every render, and updating state like a progress bar without triggering renders.

Learning Zustand - Server State & Data Integration | Learning Zustand