The real headache in modern app development usually isn't the libraries we pick, but the blurred boundaries between our data. We often fall into the trap of dumping everything into one big global store like Redux or Zustand just to keep things "organized," but that usually just creates a different kind of mess.
If you want to test if your data is actually in the right place, ask yourself this: "If the internet cuts out and the user refreshes the page, who still holds the truth?"
If the answer is the database or an API, that’s server state. It belongs in
TanStack Query or a framework loader like Remix or Next.js. But if that data
only exists within the current browser tab, it’s client state. Use useState
for that, or stick it in the URL if you need someone to be able to share a link.
A risky pattern I see a lot is fetching data and immediately shoving it into a global store. The intention is to keep things in sync, but you end up accidentally building your own custom caching system—one that lacks proper stale time or invalidation logic. We often end up blaming React for too many re-renders, when the real issue is how we’re actually managing the data.
Drawing a clear line makes much more sense. For example, a search input or a filter should be URL state, while the actual search results are server state. If you store the results in Zustand but keep the filters in local state, the app will feel broken when someone copies the link and sends it to a friend.
On the flip side, there’s ephemeral data that shouldn't touch a server cache at all. Things like an open modal, steps in a wizard, or an unsubmitted form draft should stay local. Once the component is gone, the data should be gone too.
Being too ambitious and trying to make every piece of data globally accessible usually just makes it impossible to tell when data has gone stale. The key is to figure out who actually "owns" a piece of data before you start deciding which library to use.
Back to the editor.