@tanstack/offline-transactions
v1.0.24
Published
Offline-first transaction capabilities for TanStack DB
Readme
@tanstack/offline-transactions
Offline-first transaction capabilities for TanStack DB that provides durable persistence of mutations with automatic retry when connectivity is restored.
Features
- Outbox Pattern: Persist mutations before dispatch for zero data loss
- Automatic Retry: Configurable retry behavior with exponential backoff + jitter by default
- 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
Installation
Web
npm install @tanstack/offline-transactionsReact Native / Expo
npm install @tanstack/offline-transactions @react-native-community/netinfoThe React Native implementation requires the @react-native-community/netinfo peer dependency for network connectivity detection.
Platform Support
This package provides platform-specific implementations for web and React Native environments:
- Web: Uses browser APIs (
window.online/offlineevents,document.visibilitychange) - React Native: Uses React Native primitives (
@react-native-community/netinfofor network status,AppStatefor foreground/background detection)
Quick Start
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:
import { startOfflineExecutor } from '@tanstack/offline-transactions'React Native / Expo:
import { startOfflineExecutor } from '@tanstack/offline-transactions/react-native'Usage (same for both platforms):
// 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()Core Concepts
Outbox-First Persistence
Mutations are persisted to a durable outbox before being applied, ensuring zero data loss during offline periods:
- Mutation is persisted to IndexedDB/localStorage
- Optimistic update is applied locally
- When online, mutation is sent to server
- On success, mutation is removed from outbox
Multi-tab Coordination
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
FIFO Sequential Processing
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
API Reference
startOfflineExecutor(config)
Creates and starts an offline executor instance.
interface OfflineConfig {
collections: Record<string, Collection>
mutationFns: Record<string, MutationFn>
storage?: StorageAdapter
maxConcurrency?: number
jitter?: boolean
beforeRetry?: (transactions: OfflineTransaction[]) => OfflineTransaction[]
onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void
onLeadershipChange?: (isLeader: boolean) => void
onlineDetector?: OnlineDetector
}OfflineExecutor
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 transactionsdispose()- Clean up resources
Error Handling
Use NonRetriableError for permanent failures:
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
}
}Advanced Usage
Custom Storage Adapter
import {
IndexedDBAdapter,
LocalStorageAdapter,
} from '@tanstack/offline-transactions'
const executor = startOfflineExecutor({
// Use custom storage
storage: new IndexedDBAdapter('my-app', 'transactions'),
// ... other config
})Manual Transaction Control
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()Migration from TanStack DB
This package uses explicit offline transactions to provide offline capabilities:
// 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!Platform Support
Web Browsers
- 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
- React Native: 0.60+ (tested with latest versions)
- Expo: SDK 40+ (tested with latest versions)
- Required peer dependency:
@react-native-community/netinfofor network connectivity detection - Storage: Uses AsyncStorage or custom storage adapters
License
MIT
