Skip to content

sqliteai/sqlite-sync-react-native

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

55 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

@sqliteai/sqlite-sync-react-native

Status: Beta npm version License: MIT

Build real-time, collaborative mobile apps that work seamlessly offline and automatically sync when online. Powered by SQLite Sync.

✨ Features

  • 🧩 Offline-First, Automatic Sync
    Wrap your app with SQLiteSyncProvider to get a local database with automatic, bi-directional cloud synchronization. Your app works fully offline, and all local changes are synced seamlessly when online.

  • πŸͺ React Hooks Designed for Sync-Aware Data
    Use hooks like useSqliteSyncQuery and useOnSqliteSync to automatically refresh your UI when changes are synced from the cloud β€” keeping your app up-to-date without boilerplate code.

  • πŸ”§ Zero-Configuration Extension Loading
    The SQLite Sync extension is automatically loaded and configured for you. No manual setup required β€” just access the full SQLite Sync API directly through the db instance.

  • πŸ“± Native-Only, Ultra-Fast
    Under the hood, we use OP-SQLite β€” a low-level, JSI-enabled SQLite engine for React Native With OP-SQLite, database operations run at near-native speed on iOS and Android.

πŸ“š Table of Contents

πŸ“‹ Requirements

⚠️ Note: This library is native-only (iOS/Android).

πŸ“¦ Installation

1. Install Dependencies

npm install @sqliteai/sqlite-sync-react-native @op-engineering/op-sqlite @react-native-community/netinfo
# or
yarn add @sqliteai/sqlite-sync-react-native @op-engineering/op-sqlite @react-native-community/netinfo

2. Platform Setup

iOS

cd ios && pod install

Android

No additional setup required. Native modules are linked automatically.

Expo

If using Expo, you must use development builds (Expo Go is not supported):

# Generate native directories
npx expo prebuild

# Run on iOS/Android
npx expo run:ios
# or
npx expo run:android

πŸš€ Quick Start

1. Set Up SQLite Cloud

  1. Create an account
    Sign up at the SQLite Cloud Dashboard.

  2. Create a database
    Follow the database creation guide.

    Ensure your tables have identical schemas in both local and cloud databases.

  3. Enable OffSync
    Configure OffSync by following the OffSync setup guide.

  4. Get credentials
    Copy your connection string and API key from the dashboard.

2. Wrap Your App

The SQLiteSyncProvider needs a createTableSql statement for each table you want to sync. This is required because tables must exist before SQLiteSync initialization.

import { SQLiteSyncProvider } from '@sqliteai/sqlite-sync-react-native';

export default function App() {
  return (
    <SQLiteSyncProvider
      connectionString="sqlitecloud://your-host.sqlite.cloud:8860/your-database"
      databaseName="myapp.db"
      apiKey="your-api-key"
      syncInterval={5000}
      tablesToBeSynced={[
        {
          name: 'tasks',
          createTableSql: `
            CREATE TABLE IF NOT EXISTS tasks (
              id TEXT PRIMARY KEY NOT NULL,
              title TEXT,
              completed INTEGER DEFAULT 0,
              created_at TEXT DEFAULT CURRENT_TIMESTAMP
            );
          `,
        },
      ]}
    >
      <YourApp />
    </SQLiteSyncProvider>
  );
}

3. Use the Hooks

The library provides specialized hooks to simplify querying, syncing, and state management.

import { useCallback } from 'react';
import {
  useSqliteDb,
  useSqliteSyncQuery,
  useTriggerSqliteSync,
  useOnSqliteSync,
} from '@sqliteai/sqlite-sync-react-native';

function TaskList() {
  // 1. DATA FETCHING: Automatically loads local data AND updates on sync
  const {
    data: tasks,
    isLoading,
    error,
  } = useSqliteSyncQuery<{ id: string; title: string }>(
    'SELECT * FROM tasks ORDER BY created_at DESC'
  );

  // 2. MANUAL SYNC: Control when to sync (e.g., Pull-to-Refresh)
  const { triggerSync, isSyncing } = useTriggerSqliteSync();

  // 3. EVENT LISTENING: React to incoming changes (e.g., Show a toast)
  useOnSqliteSync(() => {
    console.log('✨ New data arrived from the cloud!');
  });

  // 4. WRITING DATA: Use the optimized hook (no re-renders on sync)
  const { db } = useSqliteDb();

  const addTask = useCallback(
    async (title: string) => {
      if (!db) return;
      // Use cloudsync_uuid() for conflict-free IDs
      await db.execute(
        'INSERT INTO tasks (id, title) VALUES (cloudsync_uuid(), ?);',
        [title]
      );
      // Optional: Push changes to cloud immediately
      triggerSync();
    },
    [db, triggerSync]
  );

  if (isLoading) return <Text>Loading local DB...</Text>;
  if (error) return <Text>Error: {error.message}</Text>;

  return (
    <View>
      <Button
        title={isSyncing ? 'Syncing...' : 'Sync Now'}
        onPress={triggerSync}
        disabled={isSyncing}
      />

      {tasks.map((task) => (
        <Text key={task.id}>{task.title}</Text>
      ))}

      <Button title="Add Random Task" onPress={() => addTask('New Task')} />
    </View>
  );
}

🎯 API Reference

SQLiteSyncProvider

Main provider component that enables sync functionality.

Props

Prop Type Required Description
connectionString string βœ… SQLite Cloud connection string
databaseName string βœ… Local database file name
tablesToBeSynced TableConfig[] βœ… Array of tables to sync
syncInterval number βœ… Sync interval in milliseconds
apiKey string * API key for authentication
accessToken string * Access token for RLS authentication
debug boolean ❌ Enable debug logging (default: false)
children ReactNode βœ… Child components

* Either apiKey or accessToken is required

TableConfig

interface TableConfig {
  name: string; // Table name (must match cloud table)
  createTableSql: string; // CREATE TABLE SQL statement
}

Example:

{
  name: 'users',
  createTableSql: `
    CREATE TABLE IF NOT EXISTS users (
      id TEXT PRIMARY KEY NOT NULL,
      name TEXT,
      email TEXT UNIQUE,
      created_at TEXT DEFAULT CURRENT_TIMESTAMP
    );
  `
}

Important:

  • Always include IF NOT EXISTS to prevent errors if the table already exists
  • The table schema must match exactly to your remote table schema in SQLite Cloud
  • The library executes this SQL during initialization, before SQLiteSync setup

Contexts

The library provides three separate React Contexts for optimized re-renders:

SQLiteDbContext

Provides database instance and initialization errors. Rarely changes (only on init/error).

import { useContext } from 'react';
import { SQLiteDbContext } from '@sqliteai/sqlite-sync-react-native';

const { db, initError } = useContext(SQLiteDbContext);

Values:

Property Type Description
db DB | null op-sqlite database instance with SQLite Sync extension loaded
initError Error | null Fatal database error (db unavailable)

SQLiteSyncStatusContext

Provides sync status information. Changes frequently (on every sync).

import { useContext } from 'react';
import { SQLiteSyncStatusContext } from '@sqliteai/sqlite-sync-react-native';

const { isSyncing, lastSyncTime, syncError } = useContext(
  SQLiteSyncStatusContext
);

Values:

Property Type Description
isSyncReady boolean Whether sync is configured and ready
isSyncing boolean Whether sync is currently in progress
lastSyncTime number | null Timestamp of last successful sync
lastSyncChanges number Number of changes in last sync
syncError Error | null Recoverable sync error (db works offline-only)

SQLiteSyncActionsContext

Provides stable sync action functions. Never changes.

import { useContext } from 'react';
import { SQLiteSyncActionsContext } from '@sqliteai/sqlite-sync-react-native';

const { triggerSync } = useContext(SQLiteSyncActionsContext);

Values:

Property Type Description
triggerSync () => Promise<void> Function to manually trigger a sync operation
subscribe (callback: () => void) => () => void Subscribe to sync events without re-renders. Returns unsubscribe function

Note: Most users should use the specialized hooks instead of accessing contexts directly.

About the db instance:

The db property is a DB instance from @op-engineering/op-sqlite with the SQLite Sync extension loaded. This means you can:

Hooks

The library provides specialized hooks for different use cases. Choose the right hook based on what data your component needs to avoid unnecessary re-renders.

useSqliteDb()

Access the database instance and initialization errors without subscribing to sync updates. This hook is optimized for components that only need database access and won't re-render on every sync.

const { db, initError } = useSqliteDb();

if (initError) {
  return <Text>Error: {initError.message}</Text>;
}

if (!db) {
  return <Text>Loading...</Text>;
}

// Use db for queries without re-rendering on sync
const addTask = async (title: string) => {
  await db.execute(
    'INSERT INTO tasks (id, title) VALUES (cloudsync_uuid(), ?);',
    [title]
  );
};

Returns:

  • db: Database instance
  • initError: Fatal initialization error

Best for: Components that write data or need database access but don't need sync status.

useSyncStatus()

Access sync status information, use this when you need to display sync state in your UI.

const { isSyncing, lastSyncTime, syncError, isSyncReady, lastSyncChanges } =
  useSyncStatus();

return (
  <View>
    <Text>{isSyncing ? 'Syncing...' : 'Idle'}</Text>
    {lastSyncTime && (
      <Text>Last sync: {new Date(lastSyncTime).toLocaleTimeString()}</Text>
    )}
    {syncError && <Text>Sync error: {syncError.message}</Text>}
  </View>
);

Returns:

  • isSyncReady: Whether sync is configured
  • isSyncing: Whether sync is in progress
  • lastSyncTime: Last successful sync timestamp
  • lastSyncChanges: Number of changes in last sync
  • syncError: Recoverable sync error

Best for: Status displays, sync indicators, monitoring components.

useSqliteSync()

Access all sync functionality (database + status + actions). This is a convenience hook that combines all contexts.

Note: This hook will re-render on every sync operation. If you only need db/initError, use useSqliteDb() instead.

const { db, initError, isSyncing, lastSyncTime, triggerSync } = useSqliteSync();

return (
  <View>
    <Text>Database: {db ? 'Ready' : 'Loading'}</Text>
    <Button onPress={triggerSync} disabled={isSyncing} />
  </View>
);

Returns: All properties from useSqliteDb() + useSyncStatus() + triggerSync function

Best for: Components that need access to everything (use sparingly to avoid unnecessary re-renders).

useTriggerSqliteSync()

Manually trigger a sync operation.

How it works: This hook is a convenience wrapper that exposes the triggerSync function from the Provider. The actual sync logic lives in SQLiteSyncProvider to ensure that isSyncing, lastSyncTime, and lastSyncChanges state are updated correctly, allowing all hooks (useOnSqliteSync, useSqliteSyncQuery) to react properly.

const { triggerSync, isSyncing } = useTriggerSqliteSync();

// Trigger sync on button press
<Button
  onPress={triggerSync}
  disabled={isSyncing}
  title={isSyncing ? 'Syncing...' : 'Sync Now'}
/>;

useOnSqliteSync(callback)

Execute a callback when sync completes with changes.

Important:

  • This hook does NOT run on initial mount - it's an event listener for sync updates only. For initial data loading, use a separate useEffect or useSqliteSyncQuery
  • This hook uses a subscription pattern that does NOT cause re-renders when sync completes without changes
import { useEffect, useCallback } from 'react';

const loadData = useCallback(async () => {
  const result = await db?.execute('SELECT * FROM tasks;');
  setTasks(result?.rows || []);
}, [db]);

// 1. Initial Load (Run once when DB is ready)
useEffect(() => {
  if (db) loadData();
}, [db, loadData]);

// 2. Sync Updates (Run only when cloud data arrives)
useOnSqliteSync(loadData);

useSqliteSyncQuery(query)

Execute a query and automatically re-run when sync updates.

Offline-First: Runs immediately when the database is available, regardless of sync status. This ensures data loads from the local database even when offline.

Auto-Refresh: Re-runs the query automatically when cloud changes arrive via sync. Uses a subscription pattern to avoid unnecessary re-renders - only re-renders when data changes.

Loading States:

  • isLoading: True only during initial load (when there's no data yet)
  • isRefreshing: True during background updates (sync updates, manual refresh)
const { data, isLoading, isRefreshing, error, refresh } =
  useSqliteSyncQuery<Task>('SELECT * FROM tasks ORDER BY created_at DESC');

// Show full-screen spinner only on first load
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;

return (
  <>
    {/* Subtle indicator for background updates */}
    {isRefreshing && <TopBarSpinner />}

    <Button onPress={refresh} title="Refresh" />
    <FlatList
      data={data}
      renderItem={({ item }) => <TaskItem task={item} />}
      refreshControl={
        <RefreshControl refreshing={isRefreshing} onRefresh={refresh} />
      }
    />
  </>
);

πŸ”„ Alternative: op-sqlite Reactive Queries (Table-Level Granularity)

For more fine-grained control over when queries re-execute, you can use op-sqlite's reactive queries directly instead of useSqliteSyncQuery.

🚨 Error Handling

The library separates fatal database errors from recoverable sync errors to enable true offline-first operation.

Database Errors (initError)

Fatal errors that prevent the database from working at all. The app cannot function when these occur.

const { initError, db } = useSqliteDb();

if (initError) {
  return <ErrorScreen message="Database unavailable" />;
}

Common causes:

  • Unsupported platform (web, Windows, etc.)
  • Missing database name
  • Failed to open database file
  • Failed to create tables

When this happens: The db instance will be null and the app cannot work offline or online.

Sync Errors (syncError)

Recoverable errors that prevent syncing but allow full offline database access.

const { db } = useSqliteDb();
const { syncError } = useSyncStatus();

// Database still works offline even with syncError!
if (db) {
  await db.execute('INSERT INTO tasks ...');
}

// Show non-blocking warning
{
  syncError && <Banner warning={syncError.message} />;
}

Common causes:

  • Missing or invalid connection string
  • Missing or invalid API key/access token
  • SQLiteSync extension failed to load
  • Network initialization failed
  • Temporary network connectivity issues

When this happens: The db instance is available and fully functional for local-only operations. Sync will be retried on the next interval or when credentials are provided.

Important: Sync errors automatically clear on the next successful sync.

πŸ› Debug Logging

Enable detailed logging during development:

<SQLiteSyncProvider
  // ... other props
  debug={__DEV__} // Enable in development only
>

When enabled, logs:

  • Database initialization steps
  • Extension loading
  • Table creation
  • Network setup
  • Sync operations
  • Change counts

πŸ“– Examples

Check out the examples directory for complete working examples:

  • sync-demo - Basic sync demonstration with CRUD operations

πŸ”— Links

About

Offline-first React Native library with automatic sqlite-sync powered by SQLite Cloud

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages