codingstairs
NotesEDULifeContact
⌕Search⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

Get in touch

Send without signing in. Add your email if you'd like a reply.

  • Leave a message anonymously →
  • ✉ warragon112@gmail.com
  • KakaoTalk Open Chat ↗

© 2026 codingstairs

  • Notes
  • EDU
  • Search
  • Life
  • Contact
  • Legal
  • RSS
  • GitHub
Notes›frontend

The Philosophy of State Management

Published 2026-04-28· Updated 2026-05-18·0 views

The Philosophy of State Management — Client State and Server State

State management in React apps creates confusion because there are many libraries. The biggest fork, however, is not the library choice but whether the state belongs to the client or to the server.

1. Two kinds of state

The distinction TanStack Query's docs spell out is widely cited.

Client state is owned by the client. Whether a button was pressed, whether a sidebar is open, what was typed into a form.

Server state is owned by the server, and the client merely holds a copy. Other users can change it, and over time it goes stale.

Trying to handle both with the same tool gets awkward.

  • Putting server state into a Redux store means writing cache invalidation, background refetching, and stale handling by hand.
  • Forcing client state into React Query produces meaningless query keys.

2. Client state tools

Place Tool Notes
Inside a component useState, useReducer React built-ins.
Inside a component tree Context API If the value changes often, re-render cost is high.
App-global (lightweight) zustand, jotai, valtio Small, low boilerplate.
App-global (structured) Redux Toolkit, MobX DevTools, consistent patterns for large teams.

Library origins (all licensed MIT):

Tool First release Author
Redux 2015 Dan Abramov, Andrew Clark.
Redux Toolkit (RTK) 2019 Official Redux. Reduces boilerplate.
MobX 2015 Michel Weststrate. observable-based.
Recoil 2020, Meta atom/selector. Effectively frozen with the January 2025 archive notice. Successor is Jotai.
zustand 2019 pmndrs (Paul Henschel). bear store.
jotai 2020 pmndrs. atom-based, Recoil-style.
valtio 2021 pmndrs. proxy-based.
XState 2017 David Khourshid. State machines.

The model differences between zustand and jotai are often compared.

  • zustand — keep multiple values in one store, subscribe via selector. "store-centric".
  • jotai — compose small atoms. "atom-centric", explicitly citing Recoil's influence.

3. Server state tools

Server state needs caching, refetching, optimistic updates, and deduplication.

Tool First release Author
Apollo Client 2016 GraphQL-focused. Normalized cache.
SWR 2019, Vercel Named after "stale-while-revalidate" in RFC 5861.
React Query → TanStack Query 2020 (RQ v1) Tanner Linsley. Renamed to TanStack from v4 (2022).
RTK Query 2021 Bundled with Redux Toolkit.
Relay 2015 Meta. GraphQL-only.

Most libraries share the following pattern.

const { data, isLoading, error } = useQuery({
  queryKey: ["posts", page],
  queryFn: () => fetch(`/api/posts?p=${page}`).then(r => r.json()),
  staleTime: 60_000,
})
  • queryKey — cache identifier. Same key, same data.
  • staleTime — "considered fresh for this duration" (no refetch).
  • gcTime / cacheTime — how long unused cache stays in memory.
  • invalidate — invalidate related keys after a mutation so the next use refetches.

4. Relationship with Server Components

With React Server Components (Next.js App Router and similar) emerging, part of the server state can be read directly from the server at render time. In that case, a client-side cache library may not be necessary.

export default async function Page() {
  const posts = await db.post.findMany()
  return <List items={posts} />
}

Interactive state (optimistic updates, polling, infinite scroll) still needs a client cache. SWR/TanStack Query and RSC are not competitors but share the workload.

In addition, 19's useOptimistic and useActionState are creating a standard place for handling small, form-level server state.

5. zustand

import { create } from "zustand"

type Store = {
  count: number
  inc: () => void
}

export const useCounter = create<Store>((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
}))

// component
const count = useCounter((s) => s.count)
const inc = useCounter((s) => s.inc)

6. TanStack Query

import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query"

const qc = new QueryClient()

export function Root() {
  return (
    <QueryClientProvider client={qc}>
      <App />
    </QueryClientProvider>
  )
}

function Posts() {
  const { data, isPending } = useQuery({
    queryKey: ["posts"],
    queryFn: () => fetch("/api/posts").then(r => r.json()),
    staleTime: 30_000,
  })
  if (isPending) return <p>...</p>
  return <ul>{data.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
}

7. SWR

import useSWR from "swr"

const fetcher = (url: string) => fetch(url).then(r => r.json())

function Posts() {
  const { data, error, isLoading } = useSWR("/api/posts", fetcher)
  if (isLoading) return <p>...</p>
  if (error) return <p>error</p>
  return <ul>{data.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
}

8. Common pitfalls

Putting server state in Redux — possible, but cache invalidation and refetching must be coded manually. A heavy cost.

Fetching inside useEffect — fine in small apps, but concurrency, cancellation, and deduplication must be handled by hand. As pages grow, tools like SWR or TanStack Query become lighter.

Overuse of Context — when one Context changes often, the entire tree re-renders. zustand and jotai's selector model fills this place well.

queryKey design — too coarse a key broadens the invalidation scope; too fine a key explodes the invalidate combinations. Resource + identifier + filter is a recommended starting point.

The SSR problem of localStorage sync — window does not exist on the server. Even zustand's persist middleware can cause hydration mismatch, so a if (typeof window !== "undefined") guard or useEffect sync is needed.

The meaning of stale-while-revalidate — a cache model from RFC 5861 that "shows the stale response briefly and refetches in the background." SWR's name comes from there.

Closing thoughts

State management depends more on the decision of "where does this state belong" than on the choice of tool. Treating client state and server state with different tools from the start reduces operational burden. With RSC, part of server state has come closer, but interactive places still need a client cache as the answer.

Next

  • styling-tailwind
  • tauri-over-electron

We refer to the TanStack Query docs, SWR official, Redux Toolkit, zustand, Jotai, RFC 5861, Choosing the State Structure, and XState.

More in frontend

All in this category →
  • Dashboard widget uniformity — don''t leave 4 domains with 3 widgets
  • Admin UI — ResourceTable SSOT pattern
  • Page Loading UX
  • Native Integrations — OS Features
  • OCR · STT · TTS
  • SQLite — A Single-File DB for Local Apps