Offline-first transaction capabilities for TanStack DB
npm install @tanstack/offline-transactionsOffline-first transaction capabilities for TanStack DB that provides durable persistence of mutations with automatic retry when connectivity is restored.
- Outbox Pattern: Persist mutations before dispatch for zero data loss
- Automatic Retry: Exponential backoff with jitter for failed transactions
- Multi-tab Coordination: Leader election ensures safe storage access
- FIFO Sequential Processing: Transactions execute one at a time in creation order
- Flexible Storage: IndexedDB with localStorage fallback
- Type Safe: Full TypeScript support with TanStack DB integration
``bash`
npm install @tanstack/offline-transactions
`bash`
npm install @tanstack/offline-transactions @react-native-community/netinfo
The React Native implementation requires the @react-native-community/netinfo peer dependency for network connectivity detection.
This package provides platform-specific implementations for web and React Native environments:
- Web: Uses browser APIs (window.online/offline events, document.visibilitychange)@react-native-community/netinfo
- React Native: Uses React Native primitives ( for network status, AppState for foreground/background detection)
Using offline transactions on web and React Native/Expo is identical except for the import. Choose the appropriate import based on your target platform:
Web:
`typescript`
import { startOfflineExecutor } from '@tanstack/offline-transactions'
React Native / Expo:
`typescript`
import { startOfflineExecutor } from '@tanstack/offline-transactions/react-native'
Usage (same for both platforms):
`typescript
// Setup offline executor
const offline = startOfflineExecutor({
collections: { todos: todoCollection },
mutationFns: {
syncTodos: async ({ transaction, idempotencyKey }) => {
await api.saveBatch(transaction.mutations, { idempotencyKey })
},
},
onLeadershipChange: (isLeader) => {
if (!isLeader) {
console.warn('Running in online-only mode (another tab is the leader)')
}
},
})
// Create offline transactions
const offlineTx = offline.createOfflineTransaction({
mutationFnName: 'syncTodos',
autoCommit: false,
})
offlineTx.mutate(() => {
todoCollection.insert({
id: crypto.randomUUID(),
text: 'Buy milk',
completed: false,
})
})
// Execute with automatic offline support
await offlineTx.commit()
`
Mutations are persisted to a durable outbox before being applied, ensuring zero data loss during offline periods:
1. Mutation is persisted to IndexedDB/localStorage
2. Optimistic update is applied locally
3. When online, mutation is sent to server
4. On success, mutation is removed from outbox
Only one tab acts as the "leader" to safely manage the outbox:
- Leader tab: Full offline support with outbox persistence
- Non-leader tabs: Online-only mode for safety
- Leadership transfer: Automatic failover when leader tab closes
Transactions are processed one at a time in the order they were created:
- Sequential execution: All transactions execute in FIFO order
- Dependency safety: Avoids conflicts between transactions that may reference each other
- Predictable behavior: Transactions complete in the exact order they were created
Creates and starts an offline executor instance.
`typescript`
interface OfflineConfig {
collections: Record
mutationFns: Record
storage?: StorageAdapter
maxConcurrency?: number
jitter?: boolean
beforeRetry?: (transactions: OfflineTransaction[]) => OfflineTransaction[]
onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void
onLeadershipChange?: (isLeader: boolean) => void
}
#### Properties
- isOfflineEnabled: boolean - Whether this tab can persist offline transactions
#### Methods
- createOfflineTransaction(options) - Create a manual offline transactionwaitForTransactionCompletion(id)
- - Wait for a specific transaction to completeremoveFromOutbox(id)
- - Manually remove transaction from outboxpeekOutbox()
- - View all pending transactionsnotifyOnline()
- - Manually trigger retry executiondispose()
- - Clean up resources
Use NonRetriableError for permanent failures:
`typescript
import { NonRetriableError } from '@tanstack/offline-transactions'
const mutationFn = async ({ transaction }) => {
try {
await api.save(transaction.mutations)
} catch (error) {
if (error.status === 422) {
throw new NonRetriableError('Invalid data - will not retry')
}
throw error // Will retry with backoff
}
}
`
`typescript
import {
IndexedDBAdapter,
LocalStorageAdapter,
} from '@tanstack/offline-transactions'
const executor = startOfflineExecutor({
// Use custom storage
storage: new IndexedDBAdapter('my-app', 'transactions'),
// ... other config
})
`
`typescript`
const executor = startOfflineExecutor({
maxConcurrency: 5,
jitter: true,
beforeRetry: (transactions) => {
// Filter out old transactions
const cutoff = Date.now() - 24 60 60 * 1000 // 24 hours
return transactions.filter((tx) => tx.createdAt.getTime() > cutoff)
},
// ... other config
})
`typescript
const tx = executor.createOfflineTransaction({
mutationFnName: 'syncData',
autoCommit: false,
})
tx.mutate(() => {
collection.insert({ id: '1', text: 'Item 1' })
collection.insert({ id: '2', text: 'Item 2' })
})
// Commit when ready
await tx.commit()
`
This package uses explicit offline transactions to provide offline capabilities:
`typescript
// Before: Standard TanStack DB (online only)
todoCollection.insert({ id: '1', text: 'Buy milk' })
// After: Explicit offline transactions
const offline = startOfflineExecutor({
collections: { todos: todoCollection },
mutationFns: {
syncTodos: async ({ transaction }) => {
await api.sync(transaction.mutations)
},
},
})
const tx = offline.createOfflineTransaction({ mutationFnName: 'syncTodos' })
tx.mutate(() => todoCollection.insert({ id: '1', text: 'Buy milk' }))
await tx.commit() // Works offline!
`
- IndexedDB: Modern browsers (primary storage)
- localStorage: Fallback for limited environments
- Web Locks API: Chrome 69+, Firefox 96+ (preferred leader election)
- BroadcastChannel: All modern browsers (fallback leader election)
- React Native: 0.60+ (tested with latest versions)
- Expo: SDK 40+ (tested with latest versions)
- Required peer dependency: @react-native-community/netinfo` for network connectivity detection
- Storage: Uses AsyncStorage or custom storage adapters
MIT