React Query Login: Fetch User After Login Best Practices
Optimal React Query authentication flow for Expo apps: post-login user fetch with invalidateQueries, declarative navigation, avoid useEffect races. TanStack Query endorsed patterns for production-ready auth.
Is fetching user data immediately after login an anti-pattern in React Query authentication flows?
I’m developing a clean authentication flow using Expo, React Query, and a backend login endpoint that returns only an access token (no user object). After successful login, I fetch the authenticated user data to handle navigation based on user type.
Current Implementation
useLogin Hook:
export const useLogin = () => {
const queryClient = useQueryClient()
const setToken = useAuthStore((state) => state.setToken)
return useMutation({
mutationFn: async (user: UserLogin) => await login(user),
onSuccess: async (response) => {
if (!response.success)
return Alert.alert(
i18n.t('error'),
i18n.t('errors.invalidLoginCredentials')
)
setToken(response.data.access_token)
queryClient.invalidateQueries({
queryKey: queryKeys.user.me,
})
},
onError: (error) => {
console.log('error', error)
Alert.alert(i18n.t('error'), i18n.t('errors.login'))
},
})
}
useGetUser Hook:
export const useGetUser = () => {
const token = useAuthStore(state => state.token);
return useQuery({
queryKey: queryKeys.user.me,
queryFn: async () => {
const response = await getUser();
if (!response.success)
return Alert.alert(i18n.t('error'), i18n.t('errors.user.me'))
return response.data
},
staleTime: 1000 * 60 * 1,
enabled: !!token
})
}
Login Screen Usage:
const {
mutateAsync: loginMutateAsync,
isPending: isLoginPending
} = useLogin()
const { data: user, isSuccess: isUserSuccess } = useGetUser()
const onSubmit = async ({
username,
password,
}: {
username: string
password: string
}) => {
await loginMutateAsync({ username, password })
}
useEffect(() => {
if (isUserSuccess && user) {
user.userType !== 3
? router.replace('/ownerDashboard')
: router.replace('/(tabs)/main')
}
}, [user, isUserSuccess])
This works but raises architecture concerns.
Key Questions on React Query Login Best Practices
- Is fetching user data right after login an anti-pattern?
- Should I fetch the user inside the login mutation (e.g., using
queryClient.fetchQuery) for better integration? - Is this approach production-ready, or does it create tight coupling?
- What is the recommended pattern for login → fetch user → navigate with React Query, avoiding
useEffectnavigation, race conditions, and implicit auth side effects?
Seeking guidance on optimal React Query authentication patterns for Expo/React Native apps.
No, fetching user data immediately after login is not an anti-pattern in React Query authentication flows—it’s a standard, endorsed pattern from TanStack Query maintainer TkDodo himself. Your current setup with invalidateQueries on login success, a token-enabled useQuery for the user, and Zustand for state is already close to production-ready, though it has minor coupling risks from that useEffect navigation. The optimal tweak? Stick with invalidation but add prefetching for zero-wait refetch, then handle navigation declaratively via query states to dodge races and side effects.
Contents
- Is Fetching User Data After Login an Anti-Pattern in React Query Authentication?
- Analyzing Your Current React Query Login Implementation
- Recommended React Query Authentication Flow
- Best Practices for TanStack Query Auth in Expo/React Native
- Alternatives: Fetch User Inside Mutation vs InvalidateQueries
- Handling Navigation, Race Conditions, and Production Tweaks
- Real-World Examples and Libraries
- Sources
- Conclusion
Is Fetching User Data After Login an Anti-Pattern in React Query Authentication?
Short answer: Nope. Quite the opposite. TkDodo, the guy behind React Query (now TanStack Query), explicitly calls this out as a good move in his GitHub discussion on post-login patterns. Why? Mutations like login are fire-and-forget—they handle side effects like token storage perfectly, but they don’t own your cache. Invalidating a user query post-login triggers a background refetch tied to your auth state (that enabled: !!token), keeping everything reactive and declarative.
Think about it. Your backend spits out just a token? No problem. Fetch /me separately. This decouples login from user data, avoids bloating the mutation response, and leverages RQ’s superpower: automatic refetching on invalidation. Users searching “fetch user after login React Query” land here because it scales—swap backends, add refresh tokens, whatever. Tight coupling? Only if you cram the fetch inside the mutation itself, which we’ll unpack later.
But what smells off to you? Probably that brief “flash” before the user loads, or wondering if useEffect is hacking the flow. Fair concerns. It’s not broken, just ripe for polish.
Analyzing Your Current React Query Login Implementation
Your code? Solid foundation. Let’s break it down quick.
The useLogin mutation nails the basics: onSuccess stores the token via Zustand, then invalidateQueries({ queryKey: queryKeys.user.me }). Boom—any pending or cached user query refetches if enabled. useGetUser with enabled: !!token and a 1-minute staleTime is textbook; it won’t spam your API until needed.
Login screen usage works because useGetUser lives there, watching for token changes. Post-mutation, invalidation kicks off the fetch, isSuccess flips, and useEffect navigates. No infinite loops, no obvious races if query keys match exactly.
Potential gotchas:
- useEffect coupling: It’s imperative—nav depends on query state indirectly via deps. Works, but RQ shines declarative.
- Error handling: Alert in
queryFn? Risky; queries should throw, letonErrorhandle UI. - No prefetch: User sees a spinner? Invalidate is optimistic but async—feels laggy on slow nets.
- Zustand sync: Token set triggers
enabled, good. But Expo? SwaplocalStorageforexpo-secure-storepronto.
Per TkDodo’s mutations guide, this is “explicit and correct.” Production-ready? 85% there. Tweak for 100%.
Recommended React Query Authentication Flow
Here’s the gold standard for React Query login → fetch user → navigate. Keep your split mutation/query—don’t merge 'em.
- Login mutation: Store token,
invalidateQueries(orrefetchQueriesfor immediate), optionallyprefetchQueryfor instant user data. - User query:
enabled: !!token, solidstaleTime(5-10min), unique key like['user', userId]if available. - Navigation: Ditch
useEffect. Use query states (isSuccess,data) in a parent<AuthProvider>or conditional routes. Expo Router? Leverageredirectin loaders or a root layout watcher.
Refactored useLogin:
export const useLogin = () => {
const queryClient = useQueryClient();
const setToken = useAuthStore((state) => state.setToken);
return useMutation({
mutationFn: login,
onSuccess: async (response) => {
if (!response.success) throw new Error('Invalid credentials'); // Throw early
setToken(response.data.access_token);
// Invalidate + prefetch for zero-wait
await Promise.all([
queryClient.invalidateQueries({ queryKey: queryKeys.user.me }),
queryClient.prefetchQuery({
queryKey: queryKeys.user.me,
queryFn: getUser,
}),
]);
},
onError: (error) => Alert.alert('Error', error.message),
});
};
useGetUser tweak (throw in queryFn, no Alert):
export const useGetUser = () => {
const token = useAuthStore((state) => state.token);
return useQuery({
queryKey: queryKeys.user.me,
queryFn: getUser, // Assumes it throws on !success
staleTime: 1000 * 60 * 5, // 5min, tune per needs
enabled: !!token,
retry: 1, // Fail fast on auth errors
});
};
Login screen? Drop useEffect. Parent component:
const { data: user, isSuccess, isLoading } = useGetUser();
if (isSuccess && user) {
return user.userType !== 3 ? <OwnerDashboard /> : <MainTabs />;
}
if (isLoading) return <LoadingSpinner />;
return <LoginForm />;
Reactive. No effects. From a detailed auth flow example.
Best Practices for TanStack Query Auth in Expo/React Native
Expo adds wrinkles—SecureStore over Zustand alone, Axios interceptors for token injection/401 logout. TanStack discussions push “queries as subscriptions” over Context.
Key wins:
- Token storage:
expo-secure-store+ Zustand persist. Sync on app focus:useEffect(() => { if (token) queryClient.refetchQueries({ queryKey: queryKeys.user.me }); }, [token]); - Interceptors: Auto-add
Authorization: Bearer ${token}. On 401:queryClient.clear(); router.replace('/login'); - QueryClient setup:
<QueryClientProvider client={queryClient}>withdefaultOptions: { queries: { staleTime: 5 * 60 * 1000 } }> - Logout:
queryClient.removeQueries({ predicate: ({ queryKey }) => queryKey[0] === 'user' }); setToken(null);
TkDodo in discussion #1547: Base redirects on query isSuccess/data, not manual state. Subscriptions (queries) beat Zustand for derived data—user type? Compute from data.
Race-proof: Exact keys prevent cross-user invalidations.
Alternatives: Fetch User Inside Mutation vs InvalidateQueries
Fetch in mutation (queryClient.fetchQuery post-token)? Viable, but secondary.
| Approach | Pros | Cons |
|---|---|---|
| InvalidateQueries (yours) | Declarative, reactive, handles cache staleness | Tiny delay if query not mounted |
| FetchQuery in onSuccess | Instant data, prefetch nav | Tighter coupling, manual error handling |
| setQueryData | Optimistic UI | Stales fast, no auto-refetch |
Winner: Invalidate + prefetch. TkDodo #3514: “invalidateQueries/removeQueries/prefetchQueries all work.” Fetch-in-mutation shines for non-queryable side effects, but user /me is query-perfect.
Mutations discussion #3253: RQ stays unopinionated—mutations for writes, queries for reads.
Handling Navigation, Race Conditions, and Production Tweaks
Races? Token sets → enabled flips → fetch starts, but nav waits isSuccess. Slow API? User stares at login screen.
Fixes:
- Prefetch as above—data ready before screen re-renders.
- Query key deps:
['user', { me: true }]or include token hash if paranoid. - Expo Router: Root
_layout.tsxwith<Slot />wrapped in auth guard usinguseGetUser. - Offline:
networkMode: 'online', retry on reconnect.
Production polish:
- Refresh tokens? Mutation chain: login → invalidate user + refresh.
- Multi-user? Key:
['user', userId]. - Testing: Mock
queryClientin MSW.
No implicit side effects—everything query-driven.
Real-World Examples and Libraries
Check react-query-auth library—configureAuth({ userFn: '/me' }), useLogin sets cache + token, useUser query auto-enabled. Drop-in for your flow.
Full snippet from dev.to example:
// onSuccess
queryClient.setQueryData(['user'], userData); // Optional optimistic
// But prefer invalidate for truth
TkDodo patterns scale to teams. For Expo, pair with @tanstack/react-query-persist-client for background sync.
Sources
- Post-Login Authentication Patterns — TkDodo on invalidateQueries and prefetch for user fetch after login: https://github.com/TanStack/query/discussions/3514
- React Query Subscriptions for Auth — Best practices for user queries over Context, declarative redirects: https://github.com/TanStack/query/discussions/1547
- Handling Authentication in Mutations — Guidance on login mutations, interceptors, and query separation: https://github.com/TanStack/query/discussions/3253
- React Query Authentication Flow — Complete code example with login mutation, user query, and cache management: https://dev.to/this-is-learning/react-query-authentication-flow-id2
- react-query-auth Library — Production library for token + /me fetch patterns in React Query: https://github.com/alan2207/react-query-auth
- Mastering Mutations in React Query — TkDodo’s guide validating post-mutation invalidation as best practice: https://tkdodo.eu/blog/mastering-mutations-in-react-query
Conclusion
Your React Query authentication flow is already strong—refine with prefetching, declarative navigation via query states, and Expo SecureStore for a bulletproof setup. Key takeaways: Invalidate over inline fetches, lean on enabled + staleTime for reactivity, and always prioritize cache-driven UIs to kill races and coupling. This pattern powers real apps; implement it, and you’re set for scale. Questions? Dive into those TanStack threads—they’re gold.