Mobile

Offline & Caching

What the Expo scaffold does — and honestly, does not — do when the network drops. Grit Mobile is an online-first, API-backed app with React Query caching and persistent auth. It is not an offline-first sync engine, and this page is precise about the difference.

Expo appWhen onlineWhen offlineonlineoffline readScreensReact QueryGo APIfetch + mutateRQ cachelast data shownSecureStoretoken persists
Online-firstCached fallback
Online-first: React Query serves the last cached data offline and auth persists — it is not a sync engine

The short version

The Expo scaffold does not ship a local-mirror / outbox sync engine — that is a Grit Desktop feature (local SQLite mirror + versioned outbox). On mobile, data lives on the Go API. What you get out of the box is React Query's in-memory cache (so recently viewed data stays instant and survives brief blips), fast-fail networking, and token persistence across restarts. Full offline writes are something you add deliberately.

React Query caching

Every generated hook is a React Query query, and the scaffold configures a shared client in lib/query-client.ts. Two defaults shape the offline-ish behaviour: a five-minute staleTime (data is considered fresh for five minutes, so re-visiting a screen renders cached data immediately instead of showing a spinner) and one retry on failure.

apps/expo/lib/query-client.ts
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // fresh for 5 minutes
retry: 1, // one retry before surfacing an error
},
},
});

Because the cache is in memory, it is warm for the life of the running app — navigate away and back and the data is there — but it is cleared when the app process is killed. To make it survive restarts you'd add a persister; see “Going further” below.

What actually persists

One thing genuinely survives an app restart: the auth session. Tokens are written to expo-secure-store (the iOS Keychain / Android Keystore), so users stay logged in between launches even with no network at open time.

StateWhere it livesSurvives restart?
Access / refresh tokensSecureStore (Keychain / Keystore)Yes — encrypted
Fetched resource dataReact Query cache (memory)No — cleared on kill
Theme preferenceSecureStoreYes
In-flight mutationsNone — sent live to the APINo

Networking that fails fast

A flaky connection shouldn't freeze the UI. Grit's API client wraps every request in a 15-second timeout, so a request against an unreachable API aborts instead of hanging — that timeout is specifically what keeps the splash screen from getting stuck at launch. The client also handles token refresh: on a 401 it calls /auth/refresh once and retries, and unsafe methods carry a stable idempotency key so a retried write can't double-apply.

apps/expo/lib/api.ts (excerpt)
const REQUEST_TIMEOUT_MS = 15000; // fail fast instead of hanging for minutes
async function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
return await fetch(url, { ...init, signal: controller.signal });
} finally {
clearTimeout(timer);
}
}

What happens when the network drops

offline behaviour
Screen mounts
├─ Data cached & fresh (< 5 min) → renders instantly from cache
├─ Data cached but stale → shows cache, refetches in background
└─ Nothing cached → shows loading, then an error after
the retry + 15s timeout
Writes (create / update / delete)
└─ Sent live to the API. Offline → the mutation errors; the screen
surfaces it (e.g. an Alert). There is no local queue that replays
the write when connectivity returns.

Going further: true offline support

If your product genuinely needs to work with no connection — capture data on a plane, in a warehouse, in the field — you can layer it on with standard React Query + Expo pieces. Grit doesn't generate this, but it composes cleanly with what's there:

  • Persist the query cache. Add @tanstack/react-query-persist-client with an AsyncStorage persister so cached reads survive app restarts.
  • Detect connectivity. Use @react-native-community/netinfo with React Query's onlineManager so queries pause offline and resume on reconnect.
  • Queue writes. React Query's mutation persistence / resumePausedMutations() lets you replay mutations made while offline once the device is back online.
  • Or go desktop. If offline-first is central, Grit Desktop ships a full sync engine (local SQLite mirror + outbox) today — see Building an Offline-First Desktop App.