Skip to content

React Native

Terminal window
npm install @trainstar/synchro-react-native
cd ios && pod install

Requirements: React Native 0.83+, iOS 16.0+, Android minSdk 24

The package uses the TurboModule (Codegen) architecture. It bridges to the native Swift SDK on iOS and the native Kotlin SDK on Android — there is no JavaScript SQLite driver involved.

import { SynchroClient } from '@trainstar/synchro-react-native';
const client = new SynchroClient({
dbPath: 'synchro.db',
serverURL: 'https://api.example.com',
authProvider: async () => await getToken(),
clientID: deviceId,
appVersion: '1.0.0',
});
// Initialize the native module (must be called before any other method)
await client.initialize();
ParameterTypeDefaultDescription
dbPathstringRequiredSQLite database file name or path
serverURLstringRequiredSync server base URL
authProvider() => Promise<string>RequiredReturns a JWT token for authentication
clientIDstringRequiredUnique device identifier
platformstringPlatform.OSPlatform name sent during registration
appVersionstringRequiredSemantic version of the app
syncIntervalnumber30Seconds between sync cycles
pushDebouncenumber0.5Seconds after a write before triggering push
maxRetryAttemptsnumber5Maximum retry count before entering error state
pullPageSizenumber100Rows per pull page (max 1000)
pushBatchSizenumber100Pending changes per push batch (max 1000)
snapshotPageSizenumber100Rows per snapshot page (max 1000)
// Fetch multiple rows
const rows = await client.query(
'SELECT * FROM tasks WHERE user_id = ?',
[userId]
);
// Fetch a single row
const task = await client.queryOne(
'SELECT * FROM tasks WHERE id = ?',
[id]
);

Row is typed as Record<string, unknown>. Values cross the bridge as JSON, so numbers arrive as number, strings as string, booleans as boolean, and nulls as null.

const result = await client.execute(
'INSERT INTO tasks (id, title, user_id) VALUES (?, ?, ?)',
[crypto.randomUUID(), 'Review proposal', userId]
);
// result.rowsAffected === 1
const result = await client.executeBatch([
{
sql: 'INSERT INTO tasks (id, title, user_id) VALUES (?, ?, ?)',
params: [crypto.randomUUID(), 'Write report', userId],
},
{
sql: 'INSERT INTO tasks (id, title, user_id) VALUES (?, ?, ?)',
params: [crypto.randomUUID(), 'Update docs', userId],
},
]);
// result.totalRowsAffected === 2
// Write transaction
await client.writeTransaction(async (tx) => {
await tx.execute(
'INSERT INTO tasks (id, title, user_id) VALUES (?, ?, ?)',
[taskId, 'Review proposal', userId]
);
await tx.execute(
'INSERT INTO comments (id, task_id, body, status) VALUES (?, ?, ?, ?)',
[crypto.randomUUID(), taskId, 'Looks good', 'open']
);
});
// Read transaction
const count = await client.readTransaction(async (tx) => {
const row = await tx.queryOne('SELECT COUNT(*) as c FROM tasks');
return (row?.c as number) ?? 0;
});

The Transaction interface exposes query, queryOne, and execute — the same SQL methods as the top-level client.

await client.createTable('drafts', [
{ name: 'id', type: 'TEXT', primaryKey: true },
{ name: 'content', type: 'TEXT', nullable: false },
{ name: 'created_at', type: 'TEXT', nullable: false },
]);
await client.alterTable('drafts', [
{ name: 'title', type: 'TEXT', nullable: true },
]);
await client.createIndex('drafts', ['created_at']);
const unsubscribe = client.onChange(['tasks'], () => {
console.log('tasks table changed');
});
// Later: stop observing
unsubscribe();
const unsubscribe = client.watch(
'SELECT * FROM tasks ORDER BY created_at DESC',
undefined,
['tasks'],
(rows) => {
setTasks(rows);
}
);

The callback fires immediately with the current result set, then again whenever the observed tables change.

The React Native SDK provides three hooks that wrap the observation and status APIs for use in functional components.

Reactive queries that automatically re-execute when observed tables change.

import { useQuery } from '@trainstar/synchro-react-native';
function TaskList() {
const { data, loading, error, refresh } = useQuery(
client,
'SELECT * FROM tasks ORDER BY created_at DESC',
[],
['tasks']
);
if (loading) return <ActivityIndicator />;
if (error) return <Text>Error: {error.message}</Text>;
return <FlatList data={data} renderItem={({ item }) => (
<Text>{item.name as string}</Text>
)} />;
}

Parameters:

ParameterTypeDescription
clientSynchroClientThe initialized client instance
sqlstringSQL query to execute
paramsunknown[] (optional)Bind parameters
tablesstring[] (optional)Tables to observe for changes

Returns: { data: Row[], loading: boolean, error: SynchroError | null, refresh: () => void }

Observe the sync engine status in a component.

import { useSyncStatus } from '@trainstar/synchro-react-native';
function SyncIndicator() {
const { status, retryAt } = useSyncStatus(client);
switch (status) {
case 'idle':
return <Icon name="check" color="green" />;
case 'syncing':
return <ActivityIndicator />;
case 'error':
return <Icon name="warning" color="red" />;
case 'stopped':
return <Icon name="pause" color="gray" />;
}
}

Returns: SyncStatus with { status: SyncStatusType, retryAt: Date | null }

SyncStatusType is one of: 'idle' | 'connecting' | 'syncing' | 'error' | 'stopped'

Observe the count of local changes waiting to be pushed.

import { usePendingChanges } from '@trainstar/synchro-react-native';
function PendingBadge() {
const count = usePendingChanges(client, 2000);
if (count === 0) return null;
return <Badge count={count} />;
}

Parameters:

ParameterTypeDefaultDescription
clientSynchroClientRequiredThe initialized client instance
pollIntervalnumber2000Milliseconds between polls

Returns: number — the count of pending changes

// Start sync: register, fetch schema, begin sync loop
await client.start();
// Trigger an immediate sync cycle
await client.syncNow();
// Stop the sync loop
await client.stop();
// Close the database and clean up
await client.close();
const unsubscribe = client.onStatusChange((status) => {
console.log(`Status: ${status.status}`);
if (status.retryAt) {
console.log(`Retrying at: ${status.retryAt.toISOString()}`);
}
});
const unsubscribe = client.onConflict((event) => {
console.log(`Conflict on ${event.table} record ${event.recordID}`);
console.log('Client data:', event.clientData);
console.log('Server data:', event.serverData);
});
const unsubscribe = client.onSnapshotRequired(async () => {
// Prompt the user or decide programmatically
// Return true to proceed with snapshot, false to abort
const approved = await showConfirmDialog('Full resync needed. Continue?');
return approved;
});

All errors extend the base SynchroError class:

class SynchroError extends Error {
readonly code: string;
}
ClassCodeWhen It Occurs
NotConnectedErrorNOT_CONNECTEDOperation attempted before start() completes
SchemaNotLoadedErrorSCHEMA_NOT_LOADEDSchema has not been fetched from server
TableNotSyncedErrorTABLE_NOT_SYNCEDWrite on a table not in the server schema
UpgradeRequiredErrorUPGRADE_REQUIREDServer rejected the client’s app version (HTTP 426)
SchemaMismatchErrorSCHEMA_MISMATCHClient schema hash does not match server (HTTP 409)
SnapshotRequiredErrorSNAPSHOT_REQUIREDServer indicates a full snapshot is needed
PushRejectedErrorPUSH_REJECTEDOne or more push records were rejected
NetworkErrorNETWORK_ERRORNetwork connectivity failure
ServerErrorSERVER_ERRORServer returned a non-200 HTTP status
DatabaseErrorDATABASE_ERRORSQLite operation failed
InvalidResponseErrorINVALID_RESPONSEServer response could not be decoded
AlreadyStartedErrorALREADY_STARTEDstart() called when sync is already running
NotStartedErrorNOT_STARTEDsyncNow() called before start()
TransactionTimeoutErrorTRANSACTION_TIMEOUTTransaction rolled back due to 5s inactivity

Native errors are automatically mapped to TypeScript error classes via mapNativeError(). You can catch specific error types:

try {
await client.start();
} catch (error) {
if (error instanceof UpgradeRequiredError) {
showUpgradeDialog(error.minimumVersion);
} else if (error instanceof NetworkError) {
showOfflineBanner();
} else if (error instanceof SynchroError) {
console.error(`Sync error [${error.code}]: ${error.message}`);
}
}