JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

Built for developers preparing for JavaScript, React & TypeScript interviews.

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeQuestionssystem-design
Prev

Learn the concept

Data Layer Architecture

system-design
junior
data-layer

What is the difference between client state and server state?

state-management
client-state
server-state
react-query
swr
zustand
caching
Quick Answer

Client state is data owned by the browser (UI state, form inputs, navigation) that doesn't need to be fetched. Server state is data owned by the backend (user profiles, posts, products) that's fetched over the network and cached locally. Using different tools for each (useState/Zustand for client, TanStack Query/SWR for server) prevents stale data bugs and eliminates manual cache management.

Detailed Explanation

The distinction between client state and server state is one of the most important architectural concepts in modern frontend development. Treating all state the same way (as many Redux-heavy applications did) leads to complex, bug-prone code. Understanding the difference lets you choose the right tool for each type of state.

Client State

Client state is data that originates in and is owned by the browser. It has no source of truth on a server — it exists only in the user's current session.

Examples:

  • UI state: Is the modal open? Is the sidebar collapsed? Which tab is active? Is the dropdown expanded?
  • Form state: Input values, validation errors, which fields have been touched, whether the form is dirty
  • Navigation state: Current route, scroll position, history stack, breadcrumb trail
  • User preferences: Theme (light/dark), language, layout density, accessibility settings (stored in localStorage)
  • Ephemeral state: Drag position, hover state, animation progress, selection range

Characteristics of client state:

  • Synchronous — available immediately, no loading delay
  • Owned by the browser — no API call needed to get or set it
  • Session-scoped — typically lost on page refresh (unless persisted to localStorage)
  • Single source — only one user, one browser tab, one copy

Tools for client state: useState, useReducer for component-local state; Zustand, Jotai, or Valtio for shared client state across components.

Server State

Server state is data that originates on a remote server. The frontend has a cached copy that may be stale, and multiple clients may be viewing/modifying the same data simultaneously.

Examples:

  • User data: Profile, settings, permissions, notification preferences
  • Content: Posts, comments, messages, articles, products
  • Lists: Search results, feed items, notifications, transaction history
  • Computed data: Analytics dashboards, recommendation feeds, aggregated metrics

Characteristics of server state:

  • Asynchronous — requires a network request, takes time to load
  • Shared ownership — the server is the source of truth, other users may modify it
  • Can become stale — the cached copy may not match the current server state
  • Needs loading/error states — network requests can be slow, fail, or timeout
  • Needs cache management — when to refetch, when to invalidate, how long to keep

Tools for server state: TanStack Query (React Query), SWR, Apollo Client, RTK Query. These libraries handle the complexities of server state automatically.

Why Mixing Them Causes Bugs

The classic Redux pattern stored everything in one global store, including server data:

JavaScript
// The old Redux way — manual everything
const initialState = {
  // Client state
  isSidebarOpen: false,
  activeTab: 'overview',
  
  // Server state (manually managed)
  users: [],
  usersLoading: false,
  usersError: null,
  posts: [],
  postsLoading: false,
  postsError: null,
  // ... repeat for every API resource
};

This approach forces you to manually manage loading states, error states, caching, refetching, cache invalidation, race conditions, and deduplication for every single API resource. It's hundreds of lines of boilerplate that server state libraries handle automatically.

Worse, it leads to stale data bugs: you fetch user data, store it in Redux, navigate away, come back 10 minutes later, and the UI shows outdated information because nothing triggered a refetch.

The Modern Approach

Separate the concerns:

  1. Client state with lightweight, synchronous tools (useState, Zustand)
  2. Server state with a dedicated library that handles caching, refetching, and synchronization (TanStack Query, SWR)

This separation eliminates boilerplate (no manual loading/error states), prevents stale data (automatic background refetching), and makes the codebase simpler to reason about.

Stale-While-Revalidate Pattern

Server state libraries implement the stale-while-revalidate (SWR) caching strategy:

  1. First request: Fetch from network, store in cache, show to user
  2. Subsequent requests: Return cached data instantly (no loading spinner), then refetch in the background
  3. If data changed: Update the UI seamlessly with fresh data
  4. If data unchanged: Do nothing — the cache was already correct

This gives users the speed of cached data with the freshness of real-time data. The UI never shows a loading spinner for data it's seen before.

Key Interview Distinction

Client state is synchronous, browser-owned data (UI state, forms, navigation) — use useState or Zustand. Server state is asynchronous, server-owned data (fetched content, user profiles) — use TanStack Query or SWR. Mixing them in one store (the old Redux pattern) forces manual cache management and leads to stale data bugs. The modern approach separates them completely, letting specialized tools handle each type's unique challenges.

Code Examples

Server state with TanStack Query vs manual useStateTSX
// BAD: Manual server state management with useState
// You handle loading, errors, caching, refetching yourself
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => setUser(data))
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, [userId]);
  // Problems:
  // - No caching (refetches on every mount)
  // - No background revalidation (stale data)
  // - No deduplication (2 components = 2 requests)
  // - Race conditions if userId changes quickly

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return <div>{user.name}</div>;
}

// GOOD: Server state with TanStack Query
// Library handles caching, refetching, deduplication, race conditions
import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],   // Cache key
    queryFn: () =>                // How to fetch
      fetch(`/api/users/${userId}`).then((res) => res.json()),
    staleTime: 5 * 60 * 1000,    // Fresh for 5 minutes
  });
  // Benefits:
  // - Cached: instant display on revisit (no spinner)
  // - Auto-revalidates in background after staleTime
  // - Deduplicated: 10 components using same key = 1 request
  // - Race conditions handled automatically
  // - Shared across components via cache key

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return <div>{user.name}</div>;
}

Real-World Applications

Use Cases

Social Media Feed

Server state (posts, comments, likes) cached with TanStack Query and revalidated on focus; client state (which post's comment box is open, draft comment text) managed with useState — clean separation prevents stale likes and lost drafts

E-commerce Cart

Cart items are server state (synced with backend for persistence across devices) managed with React Query mutations and optimistic updates; cart drawer open/closed is client state managed with useState

Dashboard with Filters

Dashboard data (metrics, charts) is server state fetched with query keys that include filter parameters; active filters, date range selection, and chart zoom level are client state managed with Zustand

Mini Projects

State Separation Refactor

intermediate

Take a Redux-based todo app that manages everything in one store and refactor it to use Zustand for UI state (filter selection, edit mode) and TanStack Query for server state (todo items from API)

Cache Inspector

intermediate

Build a dev tool overlay that displays the current TanStack Query cache (keys, stale status, last updated) alongside Zustand client state — visualizing the separation in real time

Industry Examples

TanStack

TanStack Query was created specifically to solve the server state problem — replacing hundreds of lines of Redux loading/error/caching boilerplate with declarative hooks that handle caching, deduplication, and background revalidation automatically

Vercel

SWR (stale-while-revalidate) library was built by the Next.js team to pair with React's data fetching patterns — optimized for the stale-while-revalidate HTTP cache strategy

Airbnb

Migrated from Redux to Apollo Client for server state management and React local state for UI concerns, eliminating significant boilerplate and improving data caching across their booking flow

Resources

TanStack Query — Overview

docs

Kent C. Dodds — Application State Management with React

article

SWR — React Hooks for Data Fetching

docs
Previous
What makes a good component API for a design system?
Prev