Mobile offline
React Query persistence.
Mobile users open the app on the subway. The bookmark list from this morning should still be there. React Query has a persister plugin that makes this nearly free β 20 lines of setup, and your cached data survives a cold start.
The pattern
React Query persistence loop
Setup β one file, ~20 lines
import { QueryClient } from '@tanstack/react-query'import { persistQueryClient } from '@tanstack/react-query-persist-client'import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'import AsyncStorage from '@react-native-async-storage/async-storage'export const queryClient = new QueryClient({defaultOptions: {queries: {gcTime: 1000 * 60 * 60 * 24, // 24h β must outlive persistence windowstaleTime: 1000 * 60 * 5, // 5m freshretry: 2,},},})const persister = createAsyncStoragePersister({storage: AsyncStorage,key: 'app-cache',})persistQueryClient({queryClient,persister,maxAge: 1000 * 60 * 60 * 24, // 24h})
Wire it into the app root
import { QueryClientProvider } from '@tanstack/react-query'import { queryClient } from '../lib/query-client'export default function RootLayout() {return (<QueryClientProvider client={queryClient}><Stack /></QueryClientProvider>)}
That's it. Every query is now persisted. Restart the app while offline β your bookmark list is there, the product list is there, login state survives.
What gets persisted
By default: everything. To exclude sensitive data:
persistQueryClient({queryClient,persister,maxAge: 1000 * 60 * 60 * 24,dehydrateOptions: {shouldDehydrateQuery: (q) => {// Don't persist the /me query β auth state shouldn't outlive a sessionif (q.queryKey[0] === 'me') return falsereturn true},},})
Rule of thumb: persist lists, search results, and reference data. Do NOT persist payment info, tokens, or anything sensitive β that's what SecureStore is for.
The first-paint trick
When the app boots offline, React Query renders the cached data instantly. When the network comes back, it auto-refetches β silently. The user sees the "old" bookmarks first, which update if anything changed. No spinner, no jank.
Offline writes β separate problem
React Query persistence only handles READS. If the user bookmarks something offline, the mutation will fail at the network call. For now: optimistic update + a toast ("will retry when online") is good enough.
For real offline writes, you'd wire React Query's onlineManager + a mutation pause queue. Most teams skip this on mobile (toast + retry on manual user action is fine) and put the heavy queueing on desktop, where users expect it (next lesson).
Showing offline state
import NetInfo from '@react-native-community/netinfo'import { useEffect, useState } from 'react'import { View, Text } from 'react-native'export function OfflineBanner() {const [online, setOnline] = useState(true)useEffect(() => {const unsub = NetInfo.addEventListener((s) => setOnline(!!s.isConnected))return unsub}, [])if (online) return nullreturn (<View className="bg-amber-500/10 border-b border-amber-500/30 px-4 py-2"><Text className="text-amber-500 text-xs">Offline β showing your last cached data</Text></View>)}
Show this above the tab bar. It tells the user why they might see slightly old data, and removes the "is this broken?" anxiety.
Quick check
Try it
Make your bookmarks survive a flight:
- Add the persistence setup above.
- Bookmark 3 products.
- Force-quit the app, enable airplane mode.
- Reopen β bookmarks should be there instantly.
- Add the OfflineBanner. Confirm it shows in airplane mode and disappears when you reconnect.
- Disable airplane mode β confirm a silent refetch happens.
What's next
Next lesson β Desktop outbox. Desktop users expect long-running, queue-and-flush writes. We'll wire a proper outbox pattern in SQLite.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled β suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub