Data Layer Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Assume the network is unreliable: Design reads from local cache first and writes as async reconciliation - not as blocking fetch gates on every screen.
Publish a UX contract: Users must always know whether data is live, stale, syncing, or unavailable - agree on copy with design before implementation.
Use NetInfo for connectivity - not fetch probes: @react-native-community/netinfo reflects ongoing reachability; a one-time HTTP ping on mount lies in captive portals.
Hide the offline banner during unknown reachability: isInternetReachable === null on cold start is not offline - wait for a definitive false before alerting.
Wire onlineManager to NetInfo at bootstrap: TanStack Query should pause retries offline - uncapped retries on airplane mode drain battery and spam logs.
Wire focusManager to AppState: Refetch policies should respect mobile foreground/background - not desktop window.focus assumptions.
Stale-while-revalidate by default: Show cached Query data with a dataUpdatedAt label - full-screen spinners only when isPending && !data.
Distinguish offline from server errors: Different headlines, retry paths, and analytics - a 500 with cached data is not the same as airplane mode.
Accept local writes offline: Enqueue mutations with idempotencyKey - users should not lose form submits in a tunnel.
Pick storage by data shape: AsyncStorage for small JSON; SQLite for relational rows; MMKV for synchronous hot-path reads - not one store for everything.
Version every persisted schema: app:storage:meta:v1 for AsyncStorage, PRAGMA user_version for SQLite, meta:schemaVersion for MMKV - ship migrations before new shapes reach production.
Run migrations before first screen render: Boot gate in root _layout - screens must never read unmigrated keys or tables.
Recover from corruption without white-screening: safeParse + scoped key reset - quarantine outbox before clear() in field apps.
Namespace storage keys by domain: sync:outbox:v1, user:preferences:v1 - avoids accidental cross-feature wipes and eases logout scoping.
Enable SQLite WAL and foreign keys on create: PRAGMA journal_mode = WAL and foreign_keys = ON on every new database - performance and referential integrity from day one.
Use prepared statements or db.sql templates: Never concatenate user input into execAsync - SQL injection applies on device too.
Use withExclusiveTransactionAsync for multi-write bundles: Default withTransactionAsync can interleave concurrent queries - exclusive scope for inspection saves.
Add synced, updated_at, and tombstones to sync tables: Deletes must replicate - soft-delete flags beat hard removal for offline sync.
Default to server-authoritative reconciliation: Money, inventory, and compliance data should not use naive last-write-wins - document discarded edits in UX.
Pair LWW with server timestamps: Client Date.now() alone loses concurrent offline edits - replace updated_at with server value on every ack.
Implement optimistic UI with rollback: TanStack Query onMutate snapshots prior cache; onError restores it - per-row pending flags beat global spinners.
Persist failed mutations - optimistic UI without a queue loses creates: AsyncStorage or SQLite outbox replays on reconnect with the same idempotency key.
Register background sync in global scope: TaskManager.defineTask before React mounts; registerTaskAsync after login - flush outbox in OS deferrable windows.
Treat background tasks as a safety net: Foreground flush on NetInfo reconnect is primary - minimumInterval: 15 minutes is not real-time delivery.
Logout clears all layers: queryClient.clear(), session AsyncStorage keys, MMKV user instance, and SQLite user tables - stale cache is a security defect.
TanStack Query + NetInfo + AsyncStorage (prefs/outbox) + expo-sqlite (relational offline data). Add MMKV when profiling proves AsyncStorage is on the hot path. Add expo-background-task when field apps need deferred flush.
AsyncStorage queue for dozens of small JSON patches. SQLite when the outbox references relational rows, attachments, or thousands of pending items - see AsyncStorage Patterns and expo-sqlite.
Rarely for single-device field capture. Adopt when true multi-user simultaneous editing is a product requirement - see Sync Strategies.
Query owns server-state cache and optimistic mutations. This section owns durability, sync, and OS lifecycle - cross-read ../state-management/tanstack-query/tanstack-query.md.
Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 19, 2026