We use these words interchangeably all the time.
"The browser caches it." "React Query caches the response." "We memoize that calculation." "Service worker cache." "In-memory cache."
Cache. Memory. Memoization. Same bucket, different labels.
They're not the same thing. And the difference isn't pedantic — it's architectural, and it changes how you should design systems.
Start with what they actually are
Memory is a place where a system stores something it owns. The value exists because the system put it there, and the system is the source of truth for what that value is. If memory says X, X is true, by definition, because nothing external defines X.
Cache is a place where a system stores a copy of something whose truth lives somewhere else. The cache is never the source of truth. It's a bet — a bet that the copy still matches the original closely enough to be useful.
That's the entire distinction. Everything else follows from it.
Why this distinction has teeth
If you store something in memory, there is no concept of "going stale." A variable in your JavaScript runtime doesn't expire. let x = 5 doesn't have a TTL. The value is correct until you explicitly change it, because nothing else has authority over it.
If you store something in a cache, staleness is not a bug — it's the entire reason the structure exists in the first place. A cache without a staleness model isn't a cache. It's just memory pretending to be a cache, and it will eventually be wrong in a way nobody designed for.
This is the part frontend engineers skip past constantly.
React state vs React Query cache
useState is memory. The component owns the value. There is no external source of truth this value is trying to track. If you call setCount(5), count is 5. Permanently, until you change it again. No invalidation model needed because there's nothing to invalidate against.
useQuery from React Query is cache. The value it holds is not owned by your component — it's a snapshot of data that lives on a server, fetched at some point in the past. React Query doesn't trust that snapshot indefinitely. That's why staleTime exists. That's why refetchOnWindowFocus exists. That's why every cached entry has an implicit question attached to it: is this still true?
The mental model shift: memory answers "what is the value." Cache answers "what was the value, and how confident am I that it still is."
Redux vs RTK Query — the same codebase, two philosophies
This gets interesting because Redux Toolkit lets you do both inside the same store, and a lot of teams don't realize they're switching philosophies mid-app.
A Redux slice for UI state — isModalOpen, selectedTab, currentStep — is memory. Nobody asks if isModalOpen is "stale." It's either true or false because your application set it directly. There's no external reality it's trying to mirror.
RTK Query's cached API responses, sitting in the same Redux store, are cache. They have lastFetchedAt timestamps. They have invalidation tags. They get refetched when a mutation runs. The data isn't owned — it's borrowed from the server and tracked with suspicion.
Look in your Redux DevTools sometime and notice: some slices have no concept of expiry. Others — usually the RTK Query slices — carry metadata about freshness. That's not an implementation detail. That's two different architectural categories sharing one storage mechanism.
The browser HTTP cache is the purest example
This one is almost a textbook case because the browser is explicit about it.
Cache-Control: max-age=3600 is the browser literally encoding: "this is not the truth, it's a copy, and the copy is good enough for 3600 seconds." After that, the browser doesn't trust its own copy anymore. It revalidates.
ETag headers exist purely to answer "is my copy still accurate?" without re-downloading the whole resource. That's the cache asking the source of truth a cheap question instead of assuming.
There's no equivalent concept for, say, a value stored in localStorage that you wrote and control entirely. localStorage.setItem('theme', 'dark') doesn't go stale. It's memory. You own it. Nothing external invalidates it except you.
Why conflating them breaks systems
The actual cost of treating cache as memory: you stop asking "is this still true?" You start trusting a copy as if it were the source.
This is how stale UI bugs happen — a cached value gets treated like owned state, gets read without revalidation, and the user sees data that diverged from reality minutes ago.
The actual cost of treating memory as cache: you start adding unnecessary invalidation logic, staleness checks, and revalidation triggers to a value that was never uncertain in the first place. This is how you end up with useEffect chains trying to "sync" a piece of state that should have just been derived directly.
Both mistakes come from the same root cause: not asking, at design time, who owns this value, and is there an external truth it needs to track.
The question to ask before choosing a storage mechanism
Before reaching for useState, Redux, React Query, SWR, localStorage, or any cache layer — ask one question:
Does an external source of truth exist for this value, separate from my application?
If yes — server data, another service's state, anything outside your control — you need a cache. Build in staleness, invalidation, and revalidation from day one. Don't treat the first fetch as permanent truth.
If no — UI state, derived values, anything your application fully owns — you need memory. Don't add expiry logic to something that can't go stale. Don't "invalidate" a value nothing external can contradict.
Most frontend bugs involving "stale data" or "state out of sync" trace back to answering this question wrong at the start — usually by treating cache like memory, and trusting a copy longer than the copy deserves to be trusted.
If this was useful, I write about frontend architecture and build systems. Follow for more.
Top comments (0)