State Has Gravity
State management in large React applications follows a predictable trajectory: you start with useState, graduate to useContext, add Zustand when context re-renders become painful, and eventually end up with a global store that models things that aren't actually global.
This isn't a failure of discipline. It's what happens when state accumulates without explicit ownership decisions.
The Audit
During a Q3 2025 performance sprint, I ran a full audit of our Zustand stores in Nubank's credit product. We had 23 stores. After the audit, we classified each slice by its natural home:
- URL state: filters, pagination, selected tab, open modal ID
- Server state: anything fetched from an API (managed by TanStack Query)
- Ephemeral local state: hover, focus, animation triggers
- Truly global state: authenticated user session, theme, feature flags
The result: 9 of 23 stores mapped cleanly to URL state. 7 were pure server cache. Only 7 actually needed to be in Zustand.
The Refactor
Moving filter and pagination state to the URL had an immediate side effect nobody planned for: shareable URLs. Customer support could reproduce exact dashboard states from user-reported links. This eliminated a whole category of tickets.
// Before: Zustand filter store
const useFilterStore = create((set) => ({
status: 'all',
dateRange: null,
setStatus: (s) => set({ status: s }),
}))
// After: TanStack Router search params
const { status, dateRange } = Route.useSearch()
The migration took four sprints. Bundle size dropped 28kb. Memory profiler showed a 31% reduction in heap allocations during heavy filter interactions.
The Principle
Every piece of state should live at the boundary that owns it. URL owns navigation state. The server owns persistent data. Local UI owns ephemeral interactions. Global stores own things that genuinely span the entire application lifetime.
If you can answer "who is the source of truth?", you know where the state belongs.