Data Management vs State Management in React and Why Mixing Them Hurts
August 2025 · Derick Zr
One of the most common mistakes I see in React apps is storing server data in a client global state.
For example: putting the user profile inside React Context, Zustand, or Redux.
At first, it feels fine — you fetch the user once, save it in a global store, and use it anywhere in your app. But over time, this creates problems:
- The data can become stale unless you manually re-fetch it.
- You have to handle loading, error states, and caching yourself.
- You end up re-inventing what libraries like TanStack Query, SWR, or RTK Query already do — but less efficiently.
Data Management (Server State)
Server state is data that:
-
Lives in your backend (not the browser)
-
Can change without your app knowing
-
Needs features like caching, re-fetching, pagination, and background updates
Examples:
-
User profile
-
Product catalog
-
Analytics reports
For this, I use TanStack Query.
It acts like a smart global store for server data:
-
Fetch once, reuse anywhere in the app
-
Auto re-fetch when the data might be stale
-
Cache between navigations
State Management (Client State)
Client state is data that:
-
Exists only in the browser
-
Is often tied to UI interactions
-
Does not need to be synced with a server
Examples:
-
Modal open/close state
-
Sidebar toggle
-
Active tab in a component
-
Form input values
For this, plain React state (useState
) or lightweight libraries are enough.
Example 1: User Profile — Server State
Wrong approach (mixing responsibilities)
// profileStore.js (Zustand)
import create from 'zustand';
export const useProfileStore = create((set) => ({
profile: null,
setProfile: (profile) => set({ profile }),
}));
// App.jsx
const { setProfile } = useProfileStore();
useEffect(() => {
fetch('/api/me')
.then((res) => res.json())
.then(setProfile);
}, []);
The problem here:
-
No cache invalidation
-
No automatic re-fetch
-
If another tab updates the profile, you’ll never know
Better approach with TanStack Query
import { useQuery } from '@tanstack/react-query';
function useProfile() {
return useQuery({
queryKey: ['profile'],
queryFn: () => fetch('/api/me').then((res) => res.json())
});
}
// Anywhere in your app:
const { data: profile, isLoading } = useProfile();
Benefits:
- Cache across the app
- Auto re-fetch when needed
- Handles loading & error states out of the box
Example 2: Sidebar Toggle — Client State
Here, we don’t need TanStack Query — it’s purely UI state.
function Sidebar() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(!isOpen)}>
Toggle Sidebar
</button>
{isOpen && <div className="sidebar">I’m open!</div>}
</>
);
}
This state:
- Doesn’t come from a server
- Doesn’t need caching or refetching
- Lives entirely in the browser
Key Takeaways
-
Server State (Data Management)
Use TanStack Query, SWR, or similar tools to manage data that comes from a backend. -
Client State (State Management)
Use React state or a small store for UI interactions and local-only state.
Separating the two keeps your code simpler, more predictable, and easier to maintain — especially as your app grows.
Interactive Example
See the difference in action with this interactive demo:
Server State (The Right Way)
Data from API - should be managed by TanStack Query, SWR, etc.
Client State (UI Only)
UI interactions - perfect for React useState
Current UI State
Wrong Approach: Server Data in LocalStorage
This creates stale data problems - avoid this pattern!
⚠️ Why This Is Wrong
- • Data becomes stale (not synced with server)
- • No automatic refetching
- • Manual cache invalidation needed
- • Duplicates server state management logic
Test the Difference:
- 1. Select a user (Alice) in both sections above
- 2. Click "Simulate Server Update" to change Alice's data
- 3. Notice: Server State updates automatically, LocalStorage doesn't
- 4. This is why server data should never be in localStorage!