---
description: Offline-First Development
alwaysApply: false
---

# Offline-First Development

Building mobile apps that work reliably without network connectivity.

## Principles

- **Assume unreliable network** — mobile connections are inconsistent; design for it
- **Local-first** — read from local storage, sync in background
- **Optimistic updates** — update UI immediately, reconcile later
- **Graceful degradation** — show what's available, queue what isn't

## Storage Selection

- **AsyncStorage / MMKV** — simple key-value, settings, tokens
- **SQLite / Realm** — structured data with queries; add `syncedToServer` and `lastModified` columns
- **In-memory** — temporary session data only

## Sync Strategies

- **Pull (server → device)** — fetch changes since `lastSyncTimestamp`; upsert in a transaction
- **Push (device → server)** — query unsynced rows; send each; mark synced on success; skip on network error (retry later); throw on server rejection (conflict)
- **Conflict resolution** — choose a strategy per entity: `server-wins`, `client-wins`, or `manual` (store conflict for user)

## Optimistic Updates (React Query)

```tsx
const updateTask = useMutation({
  mutationFn: api.updateTask,
  onMutate: async (newTask) => {
    await queryClient.cancelQueries({ queryKey: ['tasks'] });
    const previous = queryClient.getQueryData(['tasks']);
    queryClient.setQueryData(['tasks'], (old: Task[]) =>
      old.map((t) => (t.id === newTask.id ? newTask : t))
    );
    return { previous };
  },
  onError: (_err, _new, ctx) => queryClient.setQueryData(['tasks'], ctx?.previous),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['tasks'] }),
});
```

## Network State & Queueing

- Detect connectivity with `NetInfo.addEventListener`; expose via `useNetworkStatus` hook
- Queue mutations when offline; persist queue to storage; process FIFO when back online
- Stop processing on network error; re-throw server rejections

## Caching

- Set per-resource TTLs (images: 7d, API responses: 5min, static assets: 30d)
- Use stale-while-revalidate: return cached data immediately, refresh in background

## UI Patterns

- Show offline banner when disconnected (`useNetworkStatus`)
- Display sync indicators on unsynced items (cloud-off icon, "pending sync")
- Show pending change count with activity indicator during sync

## Anti-Patterns

- **No sync indicator** — users must know what's synced and what isn't
- **Blocking on network** — never make the user wait for a request that can be queued
- **Ignoring conflicts** — always have a conflict strategy; silent data loss is unacceptable
