@dataql/react-native
v0.5.0
Published
DataQL React Native SDK with offline-first capabilities and clean API
Downloads
319
Maintainers
Readme
@dataql/react-native
An offline-first React Native SDK for DataQL with automatic synchronization using Drizzle ORM and Expo SQLite.
Features
- Offline-First: All data operations work offline and sync automatically when online
- Drizzle ORM: Built on top of Drizzle ORM for type-safe database operations
- Expo SQLite: Uses Expo SQLite for local storage with change listeners
- Live Queries: Real-time data updates using Drizzle's live query capabilities
- Automatic Sync: Background synchronization with configurable intervals
- Conflict Resolution: Built-in conflict detection and resolution strategies
- React Hooks: Easy-to-use React hooks for data operations
- TypeScript: Full TypeScript support with type inference
Installation
npm install @dataql/react-native drizzle-orm expo-sqlite@next
npm install -D drizzle-kit babel-plugin-inline-importSetup
1. Configure Babel and Metro
Update your babel.config.js:
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: [["inline-import", { extensions: [".sql"] }]],
};
};Update your metro.config.js:
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(__dirname);
config.resolver.sourceExts.push("sql");
module.exports = config;2. Generate Migrations
Create a drizzle.config.ts file:
import type { Config } from "drizzle-kit";
export default {
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "sqlite",
driver: "expo",
} satisfies Config;Generate migrations:
npx drizzle-kit generateQuick Start
1. Initialize DataQL Client
import { DataQLClient, createDefaultConfig } from "@dataql/react-native";
const config = createDefaultConfig("https://your-worker-url.com/api");
const dataQLClient = new DataQLClient(config);
// Initialize the client
await dataQLClient.initialize();Using Custom Network Transport
The SDK supports custom network transport, just like the Core SDK:
import {
DataQLClient,
createDefaultConfig,
CustomRequestConnection,
} from "@dataql/react-native";
// Create a custom connection
const customConnection: CustomRequestConnection = {
async request(url: string, options: RequestInit): Promise<Response> {
// Your custom network implementation
// Could route through other SDKs, add authentication, etc.
console.log("Custom request to:", url);
return fetch(url, options);
},
};
// Configure with custom connection
const config = createDefaultConfig(
"https://your-worker-url.com/api",
"myapp.db",
{
customConnection,
}
);
const dataQLClient = new DataQLClient(config);
// Or set it later
dataQLClient.setCustomConnection(customConnection);2. Use in React Components
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useQuery, useMutation, useSync } from '@dataql/react-native';
function UserList() {
const { data: users, loading, error, refetch } = useQuery('users');
const { create, update, delete: deleteUser } = useMutation();
const { sync, syncStatus, isOnline } = useSync();
const handleCreateUser = async () => {
await create('users', {
name: 'John Doe',
email: '[email protected]',
});
refetch();
};
return (
<View>
<Text>Users ({isOnline ? 'Online' : 'Offline'})</Text>
<Text>Pending: {syncStatus.pendingOperations}</Text>
{loading && <Text>Loading...</Text>}
{error && <Text>Error: {error}</Text>}
{users.map(user => (
<View key={user.id}>
<Text>{user.name} - {user.email}</Text>
<Button
title="Delete"
onPress={() => deleteUser('users', user.id)}
/>
</View>
))}
<Button title="Add User" onPress={handleCreateUser} />
<Button title="Sync Now" onPress={sync} />
</View>
);
}3. Handle Migrations
import { useDatabaseMigrations } from '@dataql/react-native';
import migrations from './drizzle/migrations';
function App() {
const { success, error } = useDatabaseMigrations(
dataQLClient.getDatabase(),
migrations
);
if (error) {
return <Text>Migration error: {error.message}</Text>;
}
if (!success) {
return <Text>Migration in progress...</Text>;
}
return <UserList />;
}API Reference
DataQLClient
The main client class for managing offline data and synchronization.
const client = new DataQLClient(config);
await client.initialize();
// Data operations
await client.createOffline("users", userData);
await client.updateOffline("users", userId, updateData);
await client.deleteOffline("users", userId);
const results = await client.queryOffline("users");
// Sync operations
await client.sync();
client.startAutoSync();
client.stopAutoSync();
// Status
const status = await client.getSyncStatus();
const isOnline = client.isOnline();
// Custom network transport
client.setCustomConnection(customConnection);
client.setWorkerBinding(workerBinding);
client.clearCustomConnection();Hooks
useQuery
Query data with offline-first approach:
const { data, loading, error, refetch, isFromCache, lastUpdated } = useQuery(
"tableName",
filter
);useMutation
Perform create, update, delete operations:
const { create, update, delete, loading, error } = useMutation();
await create('users', userData);
await update('users', userId, updateData);
await delete('users', userId);Unique Document Creation
DataQL React Native supports createUnique() method through the underlying BaseDataQLClient, preventing duplicate documents based on comparable fields (excluding ID and subdocument fields).
import { useCallback } from 'react';
import { useMutation } from '@dataql/react-native';
function UserRegistration() {
const { create, loading, error } = useMutation();
const createUniqueUser = useCallback(async (userData) => {
// Note: createUnique is accessed through the base client
// You can implement this via custom hooks or direct client access
const dataQLClient = /* get your client instance */;
// Use the core SDK's createUnique functionality
const userCollection = dataQLClient.collection('users', userSchema);
const result = await userCollection.createUnique({
name: userData.name,
email: userData.email,
age: userData.age,
preferences: {
theme: 'dark',
notifications: true,
},
});
if (result.isExisting) {
console.log('User already exists:', result.insertedId);
} else {
console.log('Created new user:', result.insertedId);
}
return result;
}, []);
return (
<View>
<Button
title="Register User"
onPress={() => createUniqueUser({
name: 'John Doe',
email: '[email protected]',
age: 30
})}
disabled={loading}
/>
{error && <Text>Error: {error}</Text>}
</View>
);
}Excluded from comparison:
- ID fields:
id,_id, and any field containing 'id' - Auto-generated timestamps:
createdAt,updatedAt - Subdocument objects: nested objects like
preferences,profile - Subdocument arrays: arrays of objects like
addresses,reviews
Compared fields:
- Primitive fields: strings, numbers, booleans
- Enum fields: category selections
- Simple arrays: arrays of primitive values
useSync
Manage synchronization:
const {
syncStatus,
sync,
startAutoSync,
stopAutoSync,
loading,
error,
isOnline,
isSyncing,
} = useSync();useNetworkStatus
Monitor network connectivity:
const { isOnline } = useNetworkStatus();Live Queries
DataQL React Native supports live queries with two approaches:
- DataQL-style - Consistent with other DataQL operations (
tableName,filter) - Raw Drizzle - Direct database queries for advanced use cases
Requirements:
- Database opened with
enableChangeListener: true(DataQL does this by default) - Latest versions:
drizzle-orm@^0.44.2andexpo-sqlite@^15.2.12
DataQL-Style Live Queries (Recommended):
import { useLiveQuery } from '@dataql/react-native';
function LiveUserList() {
// Same signature as useQuery - automatically updates when data changes
const { data: users, error, updatedAt } = useLiveQuery('users', { isActive: true });
return (
<View>
<Text>Active Users: {users?.length || 0}</Text>
{updatedAt && (
<Text>Last Updated: {updatedAt.toLocaleTimeString()}</Text>
)}
{error && <Text>Error: {error}</Text>}
{users?.map(user => (
<Text key={user.id}>{user.name}</Text>
))}
</View>
);
}
// Consistent with other DataQL operations:
const { data: allUsers } = useLiveQuery('users'); // All users
const { data: activeUsers } = useLiveQuery('users', { isActive: true }); // Filtered users
const { data: adminUsers } = useLiveQuery('users', { role: 'admin' }); // Admin usersRaw Drizzle Live Queries (Advanced):
import { useRawLiveQuery } from '@dataql/react-native';
function AdvancedLiveQuery() {
// Get database instance from DataQL client
const db = dataQLClient.getDatabase();
// Complex live queries with joins, aggregations, etc.
const { data: userStats, error, updatedAt } = useRawLiveQuery(
db.select({
totalUsers: count(users.id),
activeUsers: count(users.id).where(eq(users.isActive, true)),
}).from(users)
);
const { data: userProjects } = useRawLiveQuery(
db.select()
.from(projects)
.innerJoin(users, eq(projects.ownerId, users.id))
.where(eq(users.id, currentUserId))
.orderBy(desc(projects.createdAt))
);
return (
<View>
<Text>Total Users: {userStats?.totalUsers}</Text>
<Text>Active Users: {userStats?.activeUsers}</Text>
</View>
);
}API Comparison:
| Hook | Signature | Use Case |
| ----------------- | ---------------------- | ------------------------------------- |
| useQuery | (tableName, filter?) | Standard offline-first queries |
| useLiveQuery | (tableName, filter?) | Real-time updates with same API |
| useRawLiveQuery | (drizzleQuery) | Advanced queries, joins, aggregations |
Features:
- ✅ Consistent API with other DataQL operations
- ✅ Automatic re-rendering on data changes
- ✅ Built-in error handling and timestamps
- ✅ Works offline with local SQLite data
- ✅ Seamless integration with DataQL's CRUD operations
- ✅ Optimized performance for complex queries
For complete Drizzle documentation, see: Drizzle ORM Expo SQLite Guide
Configuration
DataQLReactNativeConfig
interface DataQLReactNativeConfig {
databaseName: string;
syncConfig: SyncConfig;
enableChangeListener?: boolean;
debug?: boolean;
}
interface SyncConfig {
workerUrl: string;
syncInterval: number; // milliseconds
retryCount: number;
batchSize: number;
autoSync: boolean;
// Network transport options
customConnection?: CustomRequestConnection;
workerBinding?: WorkerBinding;
}
interface CustomRequestConnection {
request(url: string, options: RequestInit): Promise<Response>;
}
interface WorkerBinding {
fetch(request: Request): Promise<Response>;
}Default Configuration
const config = createDefaultConfig("https://your-api.com", "myapp.db");
// Returns:
// {
// databaseName: 'myapp.db',
// syncConfig: {
// workerUrl: 'https://your-api.com',
// syncInterval: 30000, // 30 seconds
// retryCount: 3,
// batchSize: 50,
// autoSync: true,
// },
// enableChangeListener: true,
// debug: false,
// }Architecture
The SDK follows an offline-first architecture:
- Local SQLite Database: All data is stored locally using Expo SQLite
- Operation Queue: Changes are queued for synchronization
- Background Sync: Automatic synchronization when online
- Conflict Resolution: Handles conflicts between local and server data
- Event System: Real-time updates via event listeners
Best Practices
- Initialize Early: Initialize the DataQL client early in your app lifecycle
- Handle Offline States: Always show appropriate UI for offline states
- Monitor Sync Status: Display sync status and pending operations to users
- Error Handling: Implement proper error handling for sync failures
- Performance: Use live queries sparingly for better performance
Troubleshooting
Migration Issues
If you encounter migration errors:
- Ensure
babel-plugin-inline-importis properly configured - Check that
.sqlfiles are included in Metro resolver - Verify Drizzle configuration uses
driver: 'expo'
Sync Issues
If synchronization fails:
- Check network connectivity
- Verify worker URL configuration
- Monitor pending operations
- Check server API compatibility
License
MIT
