Skip to content
WEBFeatured

State Management: TanStack Query vs Zustand

Why you don't need Redux. Combining server state and UI state.

Umesh
6 min read
#React#State#TanStack Query#Zustand

Why not Redux

Redux added value in an era before React had stable hooks and when server state syncing was ad-hoc. Today, server-cached libraries and tiny local stores cover most needs with less boilerplate.

Modern applications have two distinct types of state:

  • Server state: Data that lives on the server (fetched via API)
  • UI state: Client-only state (modals, theme, form inputs)

Redux conflates these, leading to unnecessary complexity. The better approach: dedicated tools for each.

TanStack Query Patterns

Use TanStack Query (formerly React Query) for all server state. It handles caching, background refetching, optimistic updates, and request deduplication out of the box.

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

// Fetching with automatic caching
function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 60_000, // 1 minute
  });

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  
  return <Profile user={data} />;
}

// Mutations with optimistic updates
function UpdateProfile() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: updateUser,
    onMutate: async (newUser) => {
      await queryClient.cancelQueries(['user', newUser.id]);
      const previous = queryClient.getQueryData(['user', newUser.id]);
      
      queryClient.setQueryData(['user', newUser.id], newUser);
      return { previous };
    },
    onError: (err, newUser, context) => {
      queryClient.setQueryData(
        ['user', newUser.id],
        context.previous
      );
    },
    onSettled: (data) => {
      queryClient.invalidateQueries(['user', data.id]);
    },
  });
}

Key Benefits

  • Automatic background refetching
  • Window focus refetching
  • Retry logic with exponential backoff
  • Request deduplication
  • DevTools for debugging

Zustand for UI State

Zustand excels for local UI state: toggles, modal open state, or ephemeral editor content. It's tiny (1KB), serializable, and composes well with Query-driven data.

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

// Simple UI store
const useUIStore = create(
  persist(
    (set) => ({
      theme: 'dark',
      sidebarOpen: true,
      setTheme: (theme) => set({ theme }),
      toggleSidebar: () => set((state) => ({ 
        sidebarOpen: !state.sidebarOpen 
      })),
    }),
    { name: 'ui-storage' }
  )
);

// Usage in component
function Sidebar() {
  const { sidebarOpen, toggleSidebar } = useUIStore();
  
  return (
    <aside className={sidebarOpen ? 'open' : 'closed'}>
      <button onClick={toggleSidebar}>Toggle</button>
    </aside>
  );
}

The Combined Approach

In practice, most apps use both:

  • TanStack Query: All API calls, server state, cached data
  • Zustand: UI preferences, form state, ephemeral flags

This architecture eliminated 90% of our Redux boilerplate while improving type safety and developer experience.

No more actions, reducers, or middleware—just hooks that work naturally with React's component model.

U

About Umesh

Building the future of AI-powered applications. Specializing in RAG systems, multi-agent architectures, computer vision, and high-performance web platforms.

init.contact

LET'S BUILD

SOMETHING EPIC.

hello@ewumesh.com