API Integration & Offline
Mobile apps need to handle slow networks, offline states, and large lists gracefully. In this course, you will learn to use TanStack Query for data fetching, implement pull-to-refresh and infinite scroll, and add offline support with persistent caching.
TanStack Query in React Native
If you've used TanStack Query (React Query) on the web, good news — it works identically in React Native. The sameuseQuery, useMutation, and useInfiniteQuery hooks are available with the exact same API.
useState + useEffect + loading/error/data state manually, you call useQuery and get everything for free.The only difference on mobile is that you get additional patterns like pull-to-refresh and infinite scroll that are specific to mobile UX conventions.
Challenge: Find the Query Provider
Open the root layout of the Expo app. Find where QueryClientProvider is set up. What configuration options are passed to the QueryClient?
Fetching Data
Use useQuery to fetch a list of resources from the API:
import { useQuery } from '@tanstack/react-query'
import { api } from '@/lib/api'
export default function TasksScreen() {
const { data, isLoading, error } = useQuery({
queryKey: ['tasks'],
queryFn: () => api.get('/api/tasks'),
})
if (isLoading) return <ActivityIndicator />
if (error) return <Text>Error loading tasks</Text>
return (
<FlatList
data={data.data}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<View style={{ padding: 16 }}>
<Text style={{ color: '#e8e8f0' }}>{item.title}</Text>
</View>
)}
/>
)
}FlatList instead of ScrollView for lists of data.FlatList only renders items that are visible on screen, which is critical for performance with large datasets.Challenge: Find a useQuery Hook
Find a useQuery hook in the scaffolded mobile app. What queryKey does it use? What endpoint does the queryFn call?
Creating Data
Use useMutation to create, update, or delete data. The key pattern is cache invalidation — after a successful mutation, you tell React Query to refetch the relevant queries so the list updates automatically.
import { useMutation, useQueryClient } from '@tanstack/react-query'
export function useCreateTask() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: { title: string; description: string }) =>
api.post('/api/tasks', data),
onSuccess: () => {
// Refetch the tasks list after creating a new one
queryClient.invalidateQueries({ queryKey: ['tasks'] })
},
})
}
// In a component:
const createTask = useCreateTask()
createTask.mutate({ title: 'New Task', description: 'Do this' })useQuery to automatically refetch the list. The UI updates without manual state management.Challenge: Find a useMutation Hook
Find a useMutation hook in the scaffolded code. What happens in the onSuccess callback? Which query keys are invalidated?
Pull-to-Refresh
Combine React Native's RefreshControl with the query's refetch function:
import { RefreshControl, FlatList } from 'react-native'
export default function TasksScreen() {
const { data, isLoading, isRefetching, refetch } = useQuery({
queryKey: ['tasks'],
queryFn: () => api.get('/api/tasks'),
})
return (
<FlatList
data={data?.data}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => <TaskItem task={item} />}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={refetch}
tintColor="#6c5ce7"
/>
}
/>
)
}tintColor prop sets the color of the loading spinner. Use your app's accent color so it matches your theme.Challenge: Add Pull-to-Refresh
Add a RefreshControl to a list screen in the app. Pull down — does the loading spinner appear? Does the data refresh when you release?
Infinite Scrolling
Use useInfiniteQuery with FlatList's onEndReached callback:
import { useInfiniteQuery } from '@tanstack/react-query'
export default function TasksScreen() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['tasks'],
queryFn: ({ pageParam = 1 }) =>
api.get('/api/tasks?page=' + pageParam + '&page_size=20'),
initialPageParam: 1,
getNextPageParam: (lastPage) =>
lastPage.meta.page < lastPage.meta.pages
? lastPage.meta.page + 1
: undefined,
})
// Flatten all pages into a single array
const tasks = data?.pages.flatMap((page) => page.data) ?? []
return (
<FlatList
data={tasks}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => <TaskItem task={item} />}
onEndReached={() => {
if (hasNextPage) fetchNextPage()
}}
onEndReachedThreshold={0.5}
ListFooterComponent={
isFetchingNextPage ? <ActivityIndicator /> : null
}
/>
)
}The getNextPageParam function tells React Query how to get the next page. It reads the meta object from the API response (which Grit's paginated endpoints always include) and returns the next page number. When there are no more pages, it returns undefined and hasNextPage becomes false.
Challenge: Test Infinite Scroll
If you have enough records (create 20+ via the API), test infinite scroll by scrolling to the bottom of a list. Does the loading spinner appear? Do new items load automatically?
Offline Support
TanStack Query caches all fetched data in memory by default. This means if the user navigates away from a screen and comes back, the data is shown instantly from cache (and refetched in the background). For persistent offline support — data that survives app restarts — you use AsyncStorage as a query persister.
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
const persister = createAsyncStoragePersister({
storage: AsyncStorage,
})
// Wrap your app with PersistQueryClientProvider
export default function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
>
<YourApp />
</PersistQueryClientProvider>
)
}With this setup, when the user opens your app with no internet connection, they see the last fetched data from AsyncStorage. When connectivity returns, React Query automatically refetches in the background and updates the UI.
Challenge: Test In-Memory Cache
Load some data on a list screen, then navigate to a different tab and come back. Does the data appear instantly (from cache) or does it show a loading state?
Challenge: Test Offline Mode
Load some data, then turn on airplane mode on your device. Can you still see the cached data? Navigate between screens — does the cached data persist?
Error Handling
Mobile apps face unreliable networks more often than web apps. Your error UI should be clear and actionable — always give the user a way to retry:
export default function TasksScreen() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['tasks'],
queryFn: () => api.get('/api/tasks'),
retry: 2, // Retry failed requests twice
})
if (error) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ color: '#ff6b6b', fontSize: 16, marginBottom: 12 }}>
Could not load tasks
</Text>
<Text style={{ color: '#9090a8', marginBottom: 16 }}>
Check your internet connection and try again
</Text>
<TouchableOpacity
onPress={() => refetch()}
style={{ backgroundColor: '#6c5ce7', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 8 }}
>
<Text style={{ color: '#fff', fontWeight: '600' }}>Retry</Text>
</TouchableOpacity>
</View>
)
}
// ... render data
}retry: 2 on your queries so React Query automatically retries failed requests before showing an error. This handles transient network issues without bothering the user.Challenge: Test Error Handling
Stop the API server (Ctrl+C in the API terminal). What does the mobile app show? Is there a retry button? Start the API again and tap retry — does the data load?
Summary
Here's what you learned in this course:
- TanStack Query works identically in React Native — same hooks, same API
-
useQueryfor fetching,useMutationfor creating/updating with cache invalidation - Pull-to-refresh uses
RefreshControlwithrefetch - Infinite scroll uses
useInfiniteQuerywithonEndReached - Offline support with AsyncStorage persister keeps data across app restarts
Challenge: Build a Resource Screen
Generate a Workout resource on the API with grit generate resource Workout title:string duration:int calories:int. Build a mobile screen with a list (useQuery), create form (useMutation), and pull-to-refresh.
Challenge: Add Infinite Scroll
Create at least 25 workout records through the API or the create form. Implement infinite scroll on the workout list. Scroll to the bottom — do new items load? Add a footer loading indicator.
Enjoying the course?
Help us grow — star us on GitHub, subscribe on YouTube, and follow on LinkedIn.