Module Overview
Estimated Time: 4 hours | Difficulty: Intermediate | Prerequisites: Networking, TypeScript
- React Query setup and configuration
- Query hooks and mutations
- Caching strategies
- Optimistic updates
- Pagination and infinite queries
- Error handling and retry logic
React Query Setup
Installation
Configuration
The two most important settings arestaleTime (how long cached data is considered fresh) and cacheTime (how long unused data stays in memory). Setting these correctly has a direct impact on your app’s battery consumption and perceived performance — too low and you waste bandwidth on redundant requests, too high and users see outdated information.
Data Fetching Hooks
Basic Query
Component Usage
Mutations
Mutations are for operations that change data on the server — creating, updating, or deleting records. While queries are read-only and can be freely cached, retried, and deduplicated, mutations are write operations with side effects. React Query gives you a structured lifecycle (onMutate, onError, onSettled) that makes implementing optimistic updates and rollbacks straightforward.
Basic Mutation
Mutation with Loading State
Pagination
Infinite scrolling is the dominant pattern for mobile lists — users expect to scroll continuously and see new content loaded automatically, rather than tapping “Page 2” buttons. React Query’suseInfiniteQuery manages this seamlessly: it tracks page cursors, accumulates results across pages, and provides fetchNextPage / hasNextPage for your scroll handler.
Infinite Query
Paginated List Component
Advanced Patterns
Dependent Queries
Dependent queries let you chain data fetching — “fetch X, then use X’s result to fetch Y.” Theenabled option is the key: when set to false, the query sits idle. When the dependency resolves and enabled becomes true, the query fires automatically.
Parallel Queries
Dependent Queries with useQueries
Cache Management
Prefetching
Manual Cache Updates
Error Handling
Global Error Handler
Query Retry Logic
Optimistic Updates
Optimistic updates are the secret to making your app feel instant. The idea: when a user performs an action (like toggling a status), immediately update the UI as if the server already confirmed it, then sync with the server in the background. If the server rejects the change, roll back to the previous state. This pattern is especially important on mobile where network latency is unpredictable. A 300ms delay that is barely noticeable on desktop WiFi feels sluggish on a cellular connection.Complete Optimistic Update Pattern
Choosing the Right Data Fetching Strategy
Not every screen needs the same fetching approach. The decision depends on how frequently the data changes, how critical freshness is to the user experience, and how expensive the API call is.
Decision framework:
- How often does this data change on the server? If the answer is “rarely,” set a long staleTime. If “constantly,” keep it short or use WebSocket subscriptions instead of polling.
- What happens if the user sees stale data? For a social feed, stale by 30 seconds is fine. For an account balance, stale by 30 seconds could cause a failed transaction.
- How expensive is the API call? If the endpoint is slow or rate-limited, cache aggressively and prefetch on navigation intent rather than on mount.
- Is the user on mobile data? This is the argument for longer staleTime defaults in React Native versus web apps. Every unnecessary refetch costs battery and cellular data.
React Query vs. Alternatives
A common question is when React Query is the right tool versus alternatives. Here is how they compare for mobile data fetching:
When to use React Query: REST or GraphQL APIs, teams that want powerful caching without Redux boilerplate, apps where offline support and background refetching matter.
When to consider alternatives: SWR if you want minimal bundle size and simpler API. Apollo Client if your backend is exclusively GraphQL and you need normalized caching. RTK Query if your app already uses Redux for client state.
Edge Cases and Gotchas
Race Conditions with Dependent Queries
When a parent query’s data changes (e.g., user switches accounts), dependent queries still hold cached data from the previous account. If you do not clear the cache on account switch, a user could briefly see another user’s data.Stale Closures in Mutation Callbacks
A subtle bug: if youronSuccess callback references component state via a closure, it captures the state value at the time the mutation was initiated, not when it completes. For mutations that take several seconds, the state may have changed.
Infinite Query Memory Growth
useInfiniteQuery accumulates all fetched pages in memory. For feeds with hundreds of pages, this can consume significant memory on low-end devices. React Query v5 introduced maxPages to cap this:
Network Reconnection Thundering Herd
When a mobile device transitions from airplane mode or a tunnel back to connectivity, React Query’srefetchOnReconnect fires for every stale query simultaneously. On an app with 50+ query keys, this can overwhelm your API.
Best Practices
Set Proper Stale Times
Don’t refetch too often - configure staleTime based on data volatility
Use Optimistic Updates
Improve UX by updating UI before server responds
Handle Errors Gracefully
Implement proper error boundaries and retry logic
Invalidate After Mutations
Always invalidate queries after mutations that change data
Additional Practices for Production
- Use query key factories. Instead of spreading string arrays across your codebase, centralize query keys in a factory object. This prevents typos, makes invalidation patterns discoverable, and keeps your code DRY:
-
Separate query hooks from components. Every
useQuerycall should live in its own custom hook file, not inline in a component. This makes queries reusable, testable in isolation, and keeps components focused on rendering. -
Set
retry: falsefor mutations. Queries are safe to retry because they are read-only. Mutations are not — retrying aPOST /orderscould create duplicate orders. Setretry: falseorretry: 1for mutations, and use idempotency keys on the server side. -
Use
placeholderDatainstead of separate loading states for detail screens. When navigating from a list to a detail screen, seed the detail query with the list item data so the screen renders instantly while the full data loads in the background:
Next Steps
Module 16: Local Storage & Databases
Learn to persist data locally with AsyncStorage and SQLite