For years, the go-to solution for managing global state in React apps was Redux (and later, lighter options like Zustand). But in many cases, especially when the state comes directly from the server, you don’t need a state management library at all.
This post will walk through a common scenario: keeping a user profile available across the frontend. We’ll see how TanStack Query makes this effortless (with caching, invalidation, and persistence) without Redux or Zustand.
🧩 The Problem
Imagine you have an endpoint /api/me
that returns the authenticated user’s profile. You want this profile data:
- Available across your app (navbar, settings page, etc.)
- Persisted in memory, without refetching on every render
- Revalidated only when necessary (e.g., after updating profile)
- Cleared out on logout
Traditionally, we’d reach for Redux or Zustand to:
- Fetch profile once
- Store it in global state
- Manually update/remove when needed
But TanStack Query does this out of the box.
⚡ Setting Up TanStack Query
First, install the dependencies:
npm install @tanstack/react-query axios
Wrap your app with the QueryClientProvider
:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
export function App() {
return (
<QueryClientProvider client={queryClient}>
<YourRoutes />
</QueryClientProvider>
);
}
📦 Defining the Profile Query
Instead of scattering query config across your codebase, it’s best to define a query object and re-use it everywhere:
// profileQuery.ts
import { queryOptions } from '@tanstack/react-query';
import axios from 'axios';
async function fetchProfile() {
const { data } = await axios.get('/api/me');
return data;
}
export const profileQuery = queryOptions({
queryKey: ['profile'],
queryFn: fetchProfile,
staleTime: Infinity, // profile doesn’t change often
gcTime: Infinity, // keep it cached until explicitly cleared
});
🎯 Using the Profile Across Components
Now you can import and use this query anywhere:
import { useQuery } from '@tanstack/react-query';
import { profileQuery } from './profileQuery';
function Navbar() {
const { data: profile } = useQuery(profileQuery);
return <span>Welcome, {profile?.name}</span>;
}
function SettingsPage() {
const { data: profile } = useQuery(profileQuery);
return <div>Email: {profile?.email}</div>;
}
✅ Both components read from the same cached query.
✅ No prop drilling.
✅ No Redux/Zustand boilerplate.
🔄 Revalidating Profile After Update
When you update the profile, just invalidate the query so it refetches:
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
function useUpdateProfile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (updates) => axios.put('/api/me', updates),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['profile'] });
},
});
}
This ensures your UI always has the latest profile, without manual state juggling.
🚪 Handling Logout
On logout, just remove the cached profile query:
function useLogout() {
const queryClient = useQueryClient();
return () => {
// Call your logout API
queryClient.removeQueries({ queryKey: ['profile'] });
};
}
This clears the user data instantly from memory.
📌 Takeaways
- TanStack Query can act as your global store for server state.
- Use infinite cache time for stable data like profiles, and manually invalidate on updates.
- Store query configuration (key, fn, cache time) in a query object to re-use cleanly across your app.
- For logout, simply remove the query.
👉 The result? No reducers, no actions, no global store overhead, just queries.
💡 Pro tip: Use Zustand (or React context) only for client, only UI state (e.g., theme, modal toggles). For server data, TanStack Query has you covered.
Top comments (0)