@dataql/react-native
Version:
DataQL React Native SDK with offline-first capabilities and clean API
529 lines (401 loc) • 13.4 kB
Markdown
# @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
```bash
npm install @dataql/react-native drizzle-orm expo-sqlite@next
npm install -D drizzle-kit babel-plugin-inline-import
```
## Setup
### 1. Configure Babel and Metro
Update your `babel.config.js`:
```javascript
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: [["inline-import", { extensions: [".sql"] }]],
};
};
```
Update your `metro.config.js`:
```javascript
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:
```typescript
import type { Config } from "drizzle-kit";
export default {
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "sqlite",
driver: "expo",
} satisfies Config;
```
Generate migrations:
```bash
npx drizzle-kit generate
```
## Quick Start
### 1. Initialize DataQL Client
```typescript
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:
```typescript
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
```typescript
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: 'john@example.com',
});
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
```typescript
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.
```typescript
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:
```typescript
const { data, loading, error, refetch, isFromCache, lastUpdated } = useQuery(
"tableName",
filter
);
```
#### useMutation
Perform create, update, delete operations:
```typescript
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).
```typescript
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: 'john@example.com',
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:
```typescript
const {
syncStatus,
sync,
startAutoSync,
stopAutoSync,
loading,
error,
isOnline,
isSyncing,
} = useSync();
```
#### useNetworkStatus
Monitor network connectivity:
```typescript
const { isOnline } = useNetworkStatus();
```
### Live Queries
DataQL React Native supports live queries with two approaches:
1. **DataQL-style** - Consistent with other DataQL operations (`tableName`, `filter`)
2. **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.2` and `expo-sqlite@^15.2.12`
**DataQL-Style Live Queries (Recommended):**
```typescript
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 users
```
**Raw Drizzle Live Queries (Advanced):**
```typescript
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](https://orm.drizzle.team/docs/connect-expo-sqlite)
## Configuration
### DataQLReactNativeConfig
```typescript
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
```typescript
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:
1. **Local SQLite Database**: All data is stored locally using Expo SQLite
2. **Operation Queue**: Changes are queued for synchronization
3. **Background Sync**: Automatic synchronization when online
4. **Conflict Resolution**: Handles conflicts between local and server data
5. **Event System**: Real-time updates via event listeners
## Best Practices
1. **Initialize Early**: Initialize the DataQL client early in your app lifecycle
2. **Handle Offline States**: Always show appropriate UI for offline states
3. **Monitor Sync Status**: Display sync status and pending operations to users
4. **Error Handling**: Implement proper error handling for sync failures
5. **Performance**: Use live queries sparingly for better performance
## Troubleshooting
### Migration Issues
If you encounter migration errors:
1. Ensure `babel-plugin-inline-import` is properly configured
2. Check that `.sql` files are included in Metro resolver
3. Verify Drizzle configuration uses `driver: 'expo'`
### Sync Issues
If synchronization fails:
1. Check network connectivity
2. Verify worker URL configuration
3. Monitor pending operations
4. Check server API compatibility
## License
MIT
```
```