Yeda AI Tips · #111

Español

Server State Belongs in a Cache Layer, Not Global State

You're keeping backend data in global state? That's a bug factory. Every fetched list you copy into a store is a second source of truth that starts rotting the moment it lands — and every AI-generated component that reads it inherits the rot.

Server state is not client state

Client state is data your app owns: the open tab, a form draft, dark mode. It changes only when your code changes it, so a store models it perfectly.

Server state is data you've borrowed. As Dominik Dorfmeister (TanStack Query maintainer) puts it: "your app does not own it… It is the server who owns the data." It lives remotely, arrives asynchronously, can be changed by other people without your knowledge, and can go stale in your app at any moment. The Redux docs themselves draw the line: "data fetching and caching" is really a different set of concerns than "state management."

Treating both the same is the root bug. A store snapshot of /api/todos is correct for exactly as long as nobody else touches the backend.

What a cache layer buys you

Hand-rolling fetch into a store means re-implementing, badly, everything a data-fetching cache layer — TanStack (React) Query, SWR, RTK Query, or equivalents in other ecosystems — ships by default:

ConcernHand-rolled storeCache layer
CachingManual copy, no expiryKeyed cache; inactive entries garbage-collected (default gcTime: 5 min in TanStack Query)
DeduplicationTwo components = two requestsSame key = one request, shared result
StalenessUnknown until a bug reportstaleTime marks data fresh/stale; stale data refetches in the background
InvalidationHunt every reducer that holds a copyOne call: invalidateQueries({ queryKey: ['todos'] })
Loading/error stateBoilerplate flags per endpointReturned per query, automatically

Even Redux's own tutorial concedes the boilerplate: async thunk, request, loading flags, pending/fulfilled/rejected handlers — per endpoint — and recommends RTK Query "as the default approach for data fetching in Redux apps."

The refactor in one snippet

// Before: server data smeared into a global store
useEffect(() => {
  fetch('/api/todos')
    .then(r => r.json())
    .then(data => dispatch(setTodos(data)));  // now stale forever
}, []);

// After: the cache layer owns it
const { data, isPending, error } = useQuery({
  queryKey: ['todos'],
  queryFn: () => fetch('/api/todos').then(r => r.json()),
});

// After any mutation, invalidate — every reader refetches:
queryClient.invalidateQueries({ queryKey: ['todos'] });

Ten components can call that same useQuery and the request is deduped to one. Delete the slice, the thunk, and the loading flags.

Why this matters more with AI-generated code

Coding agents pattern-match on your codebase. If server data lives in a store, every generated component invents its own fetch-then-dispatch dance, each with its own staleness bugs — components drift out of sync with the backend and each other. If the convention is "server data comes from the cache layer, keyed by resource," generated code converges on one correct pattern: a query key in, fresh data out. The architecture becomes the guardrail.

Power moves

Resources

Watched the reel? This is the deep-dive for Yeda AI Tips #111. Yeda AI designs, audits, and ships production AI-assisted engineering workflows.

Talk to us · Read the blog · Leer en español