A persistent, adapter-driven mutation orchestration engine for React Native applications.
react-native-syncflow is designed to make your React Native app robust, offline-first, and resilient to crashes and network flakiness. Instead of firing API calls and hoping for the best, you queue mutations that are guaranteed to execute eventually.
- Offline-First: Mutations are persisted immediately and processed when online.
- Retry Strategies: Configurable exponential backoff for transient failures.
- Concurrency Control: Process mutations serially or in parallel.
- Deduplication: Prevent duplicate actions (e.g., "like post") with strategies (
drop,replace,allow). - Transactions: Group mutations atomically.
- Adapter-Driven: Works with any storage (AsyncStorage, MMKV, SQLite) and network library (NetInfo).
- React Hooks:
useSyncStatus,useSyncEngine. - Type-Safe: Full TypeScript support for mutation payloads and responses.
npm install react-native-syncflow
# Peer dependencies
npm install @react-native-async-storage/async-storage @react-native-community/netinfo// types.ts
export type AppMutations = {
"user:update": { input: { id: string; name: string }; output: User };
"post:create": { input: { content: string }; output: Post };
};// sync.ts
import {
SyncEngine,
AsyncStorageAdapter,
NetInfoAdapter,
} from "react-native-syncflow";
import { AppMutations } from "./types";
export const sync = new SyncEngine<AppMutations>({
storage: new AsyncStorageAdapter(),
network: new NetInfoAdapter(),
logger: console,
});
// Register handlers (these are your API calls)
sync.register("user:update", async (data) => {
return await api.updateUser(data.id, data.name);
});
sync.register("post:create", async (data) => {
return await api.createPost(data.content);
});// App.tsx
import { SyncProvider } from 'react-native-syncflow';
import { sync } from './sync';
export default function App() {
return (
<SyncProvider engine={sync}>
<Main />
</SyncProvider>
);
}import { useSyncEngine, useSyncStatus } from 'react-native-syncflow';
import { AppMutations } from './types';
const Profile = () => {
const sync = useSyncEngine<AppMutations>();
const { isOnline, queueSize } = useSyncStatus();
const handleSave = async () => {
// This resolves immediately with an ID once queued
const mutationId = await sync.mutate('user:update', {
id: '123',
name: 'New Name'
});
console.log('Mutation queued:', mutationId);
};
return (
<View>
<Text>Queue: {queueSize}</Text>
<Button onPress={handleSave} title="Save Profile" disabled={!isOnline} />
</View>
);
};Prevent users from spamming actions.
sync.mutate(
"post:like",
{ postId: "123" },
{
deduplication: {
mode: "drop", // If 'post:like' for this key is already pending, ignore this one
dedupKey: "like:123",
},
},
);Ensure a group of mutations are queued together.
await sync.transaction(async (tx) => {
await tx.mutate("user:create", user);
await tx.mutate("settings:update", settings);
});
// Both are persisted and queued atomically at the end of the block.SyncFlow is designed to be completely decoupled. You can implement any of the following adapters to fit your tech stack.
import { StorageAdapter } from "react-native-syncflow";
import * as SQLite from "expo-sqlite";
export class SQLiteAdapter implements StorageAdapter {
private db = SQLite.openDatabaseSync("syncflow.db");
async get(key: string) {
const result = await this.db.getFirstAsync<{ value: string }>(
"SELECT value FROM sync_data WHERE key = ?",
[key],
);
return result?.value || null;
}
async set(key: string, value: string) {
await this.db.runAsync(
"INSERT OR REPLACE INTO sync_data (key, value) VALUES (?, ?)",
[key, value],
);
}
async remove(key: string) {
await this.db.runAsync("DELETE FROM sync_data WHERE key = ?", [key]);
}
}import { NetworkAdapter } from "react-native-syncflow";
export class ManualNetworkAdapter implements NetworkAdapter {
private listeners: ((online: boolean) => void)[] = [];
subscribe(listener: (online: boolean) => void) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter((l) => l !== listener);
};
}
async getCurrentState() {
try {
const res = await fetch("https://google.com", { method: "HEAD" });
return res.ok;
} catch {
return false;
}
}
// Trigger manually from your app logic
setOnline(status: boolean) {
this.listeners.forEach((l) => l(status));
}
}import { LoggerAdapter } from "react-native-syncflow";
import * as Sentry from "@sentry/react-native";
export class SentryLoggerAdapter implements LoggerAdapter {
log(msg: string) {
console.log(msg);
}
warn(msg: string) {
Sentry.captureMessage(msg, "warning");
}
error(msg: string, ...args: any[]) {
Sentry.captureException(args[0] || new Error(msg));
}
}You can define custom backoff logic. Return null to stop retrying.
const customRetry: RetryStrategy = (attempt, error) => {
if (attempt > 10) return null; // Max 10 retries
if (error.status === 401) return null; // Don't retry auth errors
return attempt * 5000; // Linear backoff: 5s, 10s, 15s...
};- Mutate: User calls
mutate. - Persist: Item is saved to Storage (status:
pending). - Process:
- Engine checks network.
- Picks item.
- Executes registered handler.
- Result:
- Success: Mark
completed(remove from queue). - Fail: Check retry strategy. If retryable, mark
pendingwithretryAfter.
- Success: Mark
MIT