Architecting the Local-First Future: Engineering Offline-Ready Apps with Expo and TanStack Query
A deep dive into building resilient, offline-ready mobile applications using Expo's modern ecosystem and TanStack Query's persistence layers. Learn how to bridge the gap between volatile state and permanent local storage.

For years, we’ve treated mobile apps as thin wrappers around an API. If the Wi-Fi dropped, the spinner started looping, and the user experience died. But in my recent projects, I’ve been obsessed with moving away from this 'cloud-first' dependency. We’re entering the era of Local-First architecture, where the local device is the primary source of truth, and the network is just an asynchronous synchronization layer.
When building with Expo, we have a massive advantage. But the real 'aha!' moment for me came when I realized how to push TanStack Query (v5) beyond simple memory caching and into a full-blown persistence engine.
Here is how I architect these systems to survive the subway tunnels and flight modes of the real world.
The Shift: From Cache to Store
Most engineers use TanStack Query for its excellent useQuery and useMutation hooks, but they treat the cache as volatile. To go local-first, we need that cache to survive app restarts and OS-level memory purges.
In my experience, the combination of react-native-mmkv (for speed) and TanStack’s persistQueryClient is the winning formula. MMKV is significantly faster than AsyncStorage because it’s a synchronous C++ library, which is crucial when you're hydrating a large state on app boot.
Step 1: Setting up the Persistent Persister
First, we need to wire up the client to talk to the disk. I prefer creating a custom persister that handles the serialization.
Step 2: The Online Manager
TanStack Query needs to know if the device is actually online. In the Expo ecosystem, @react-native-community/netinfo is the gold standard. I always wrap this in a listener so the queryClient can automatically resume paused mutations when the signal returns.
Step 3: Optimistic UI – The Secret Sauce
In a local-first app, the UI should never wait for the server. When a user hits 'Like' or 'Submit', the change should reflect instantly. We achieve this by manually updating the cache in the onMutate callback and rolling it back if the server eventually says 'no'.
This is where many devs trip up. You have to handle the 'Cancel' logic to ensure that an outgoing refetch doesn't overwrite your optimistic update.
Why This Matters for Expo Engineers
By leveraging PersistQueryClientProvider, we ensure that even if the user closes the app, their pending mutations (mutations that haven't reached the server yet) are stored and can be retried later. This turns a 'Network Error' into a 'Background Sync' task.
The Hard Truths
Engineering for local-first isn't free. You have to deal with Conflict Resolution. If a user edits a document offline on their iPhone and their iPad simultaneously, which one wins? For most of the apps I build, 'Last Write Wins' is sufficient, but for complex collaborative tools, you’ll need to look into CRDTs (Conflict-free Replicated Data Types).
Final Thoughts
Moving to a persistent architecture with Expo and TanStack Query has fundamentally changed how I approach mobile development. It’s no longer about managing loading states; it’s about managing data synchronization. The result? Apps that feel buttery smooth, instant, and—most importantly—reliable in a world of spotty 5G.
If you haven't tried persisting your query cache yet, start there. Your users will thank you for the lack of spinners.