Overview
Zero supports React Native with a custom storage provider for SQLite-based persistence. This provides better performance and reliability than IndexedDB alternatives on mobile platforms.
Installation
Install Zero and dependencies
npm install @rocicorp/zero @rocicorp/zero-react-native expo-sqlite
yarn add @rocicorp/zero @rocicorp/zero-react-native expo-sqlite
pnpm add @rocicorp/zero @rocicorp/zero-react-native expo-sqlite
For Expo projects, ensure expo-sqlite is properly configured:
npx expo install expo-sqlite
Add SQLite plugin to your app.json:
{
"expo" : {
"plugins" : [
"expo-sqlite"
]
}
}
Setup
Basic Configuration
Use the Expo SQLite storage provider instead of the default IndexedDB:
import { ZeroProvider } from '@rocicorp/zero/react' ;
import { expoSQLiteStoreProvider } from '@rocicorp/zero-react-native' ;
import { schema } from './schema' ;
function App () {
return (
< ZeroProvider
schema = { schema }
server = "https://your-zero-server.com"
userID = "user-123"
kvStore = { expoSQLiteStoreProvider }
>
< YourApp />
</ ZeroProvider >
);
}
With Authentication
import { useState , useEffect } from 'react' ;
import AsyncStorage from '@react-native-async-storage/async-storage' ;
function App () {
const [ authToken , setAuthToken ] = useState < string | null >( null );
useEffect (() => {
// Load auth token from storage
AsyncStorage . getItem ( 'authToken' ). then ( setAuthToken );
}, []);
if ( ! authToken ) {
return < LoginScreen onLogin = { setAuthToken } /> ;
}
return (
< ZeroProvider
schema = { schema }
server = "https://your-zero-server.com"
userID = "user-123"
auth = { authToken }
kvStore = { expoSQLiteStoreProvider }
>
< YourApp />
</ ZeroProvider >
);
}
Using React Hooks
All React hooks work identically in React Native:
import { useQuery , useZero } from '@rocicorp/zero/react' ;
import { View , Text , FlatList , TouchableOpacity } from 'react-native' ;
import { createBuilder } from '@rocicorp/zero' ;
import { schema } from './schema' ;
const zql = createBuilder ( schema );
function UserList () {
const [ users , { type }] = useQuery ( zql . user );
const zero = useZero ();
const handleAddUser = async () => {
await zero . mutate . user . insert ({
id: generateID (),
name: 'New User' ,
created: Date . now (),
});
};
if ( type === 'unknown' ) {
return < Text > Loading... </ Text > ;
}
return (
< View >
< FlatList
data = { users }
keyExtractor = { item => item . id }
renderItem = { ({ item }) => (
< View >
< Text > { item . name } </ Text >
</ View >
) }
/>
< TouchableOpacity onPress = { handleAddUser } >
< Text > Add User </ Text >
</ TouchableOpacity >
</ View >
);
}
Offline Support
Zero provides built-in offline support with automatic sync when the connection is restored:
import { useConnectionState } from '@rocicorp/zero/react' ;
import { View , Text } from 'react-native' ;
import { ConnectionStatus } from '@rocicorp/zero' ;
function OfflineIndicator () {
const connectionState = useConnectionState ();
if ( connectionState . status === ConnectionStatus . Connected ) {
return null ; // Don't show anything when online
}
return (
< View style = { styles . offlineBanner } >
< Text > You're offline. Changes will sync when back online. </ Text >
</ View >
);
}
Network Status Detection
Integrate with React Native’s NetInfo for better network detection:
import { useEffect } from 'react' ;
import NetInfo from '@react-native-community/netinfo' ;
import { useZero } from '@rocicorp/zero/react' ;
function NetworkManager () {
const zero = useZero ();
useEffect (() => {
const unsubscribe = NetInfo . addEventListener ( state => {
if ( state . isConnected ) {
// Trigger connection when network is available
zero . connection . connect ();
} else {
// Optionally disconnect when no network
zero . connection . disconnect ();
}
});
return unsubscribe ;
}, [ zero ]);
return null ;
}
Storage Management
Clear Local Data
import { useZero } from '@rocicorp/zero/react' ;
import { dropDatabase } from '@rocicorp/zero' ;
function SettingsScreen () {
const zero = useZero ();
const handleClearData = async () => {
// Close the current Zero instance
await zero . close ();
// Drop the database
await dropDatabase ( zero . storageKey );
// App should recreate Zero instance after this
};
return (
< TouchableOpacity onPress = { handleClearData } >
< Text > Clear Local Data </ Text >
</ TouchableOpacity >
);
}
Custom Storage Key
Use different storage keys for multiple accounts:
function App ({ userID } : { userID : string }) {
return (
< ZeroProvider
schema = { schema }
server = "https://your-zero-server.com"
userID = { userID }
storageKey = { `zero- ${ userID } ` } // Different DB per user
kvStore = { expoSQLiteStoreProvider }
>
< YourApp />
</ ZeroProvider >
);
}
Lazy Loading
Load data incrementally:
import { useState } from 'react' ;
import { FlatList } from 'react-native' ;
function InfiniteFeed () {
const [ limit , setLimit ] = useState ( 20 );
const [ posts ] = useQuery (
zql . post
. orderBy ( 'created' , 'desc' )
. limit ( limit )
);
const handleLoadMore = () => {
setLimit ( prev => prev + 20 );
};
return (
< FlatList
data = { posts }
onEndReached = { handleLoadMore }
onEndReachedThreshold = { 0.5 }
keyExtractor = { item => item . id }
renderItem = { ({ item }) => < PostItem post = { item } /> }
/>
);
}
Conditional Queries
Only load data when needed:
function UserProfile ({ userID , isVisible } : Props ) {
// Only query when screen is visible
const [ user ] = useQuery (
isVisible ? zql . user . where ( 'id' , userID ). one () : null
);
if ( ! isVisible ) {
return null ;
}
return < View > { /* render user */ } </ View > ;
}
Navigation Integration
React Navigation
import { createStackNavigator } from '@react-navigation/stack' ;
import { NavigationContainer } from '@react-navigation/native' ;
const Stack = createStackNavigator ();
function App () {
return (
< ZeroProvider
schema = { schema }
server = "https://your-zero-server.com"
userID = "user-123"
kvStore = { expoSQLiteStoreProvider }
>
< NavigationContainer >
< Stack.Navigator >
< Stack.Screen name = "Home" component = { HomeScreen } />
< Stack.Screen name = "Profile" component = { ProfileScreen } />
</ Stack.Navigator >
</ NavigationContainer >
</ ZeroProvider >
);
}
Passing Data Between Screens
// Navigation
import { useNavigation } from '@react-navigation/native' ;
function UserList () {
const navigation = useNavigation ();
const [ users ] = useQuery ( zql . user );
return (
< FlatList
data = { users }
renderItem = { ({ item }) => (
< TouchableOpacity
onPress = { () => navigation . navigate ( 'Profile' , { userID: item . id }) }
>
< Text > { item . name } </ Text >
</ TouchableOpacity >
) }
/>
);
}
// Profile screen
function ProfileScreen ({ route } : any ) {
const { userID } = route . params ;
const [ user ] = useQuery ( zql . user . where ( 'id' , userID ). one ());
return < View > { /* render user profile */ } </ View > ;
}
Real-Time Updates Example
Build a chat application:
import { useEffect , useRef } from 'react' ;
import { FlatList , TextInput , View } from 'react-native' ;
function ChatRoom ({ roomID } : { roomID : string }) {
const [ messages ] = useQuery (
zql . message
. where ( 'roomID' , roomID )
. orderBy ( 'created' , 'desc' )
. limit ( 50 )
);
const zero = useZero ();
const flatListRef = useRef < FlatList >( null );
const [ text , setText ] = useState ( '' );
// Auto-scroll to bottom on new messages
useEffect (() => {
if ( messages . length > 0 ) {
flatListRef . current ?. scrollToIndex ({ index: 0 , animated: true });
}
}, [ messages . length ]);
const sendMessage = async () => {
if ( ! text . trim ()) return ;
await zero . mutate . message . insert ({
id: generateID (),
roomID ,
text: text . trim (),
created: Date . now (),
});
setText ( '' );
};
return (
< View >
< FlatList
ref = { flatListRef }
data = { messages }
inverted
keyExtractor = { item => item . id }
renderItem = { ({ item }) => (
< View >
< Text > { item . text } </ Text >
</ View >
) }
/>
< TextInput
value = { text }
onChangeText = { setText }
onSubmitEditing = { sendMessage }
/>
</ View >
);
}
Background Sync
Background sync behavior depends on your app’s background mode configuration and platform limitations.
import { useEffect } from 'react' ;
import { AppState } from 'react-native' ;
function BackgroundSyncManager () {
const zero = useZero ();
useEffect (() => {
const subscription = AppState . addEventListener ( 'change' , nextAppState => {
if ( nextAppState === 'active' ) {
// App came to foreground - ensure connection
zero . connection . connect ();
} else if ( nextAppState === 'background' ) {
// App went to background - connection will disconnect
// after hiddenTabDisconnectDelay (default 5 minutes)
}
});
return () => subscription . remove ();
}, [ zero ]);
return null ;
}
iOS
SQLite provides better performance than web storage
Background execution is limited without background modes
Test network changes thoroughly (WiFi/Cellular)
Android
SQLite works well for large datasets
More flexible background execution
Handle app process termination gracefully
Best Practices
Use SQLite Storage : Always use expoSQLiteStoreProvider instead of IndexedDB on mobile.
Handle Offline Gracefully : Show clear offline indicators and let users continue working.
Optimize Queries : Use .limit() and pagination for large datasets to maintain performance.
Test Offline Scenarios : Thoroughly test your app’s behavior when offline and during reconnection.
Handle App Restart : Ensure your app handles the case where Zero reinitializes after app restart.
Troubleshooting
Database Errors
If you encounter SQLite errors:
import { dropDatabase } from '@rocicorp/zero' ;
// Clear corrupted database
await dropDatabase ( 'zero-user-123' );
Connection Issues
Debug connection problems:
< ZeroProvider
schema = { schema }
server = "https://your-zero-server.com"
userID = "user-123"
logLevel = "debug" // Enable verbose logging
kvStore = { expoSQLiteStoreProvider }
>
< YourApp />
</ ZeroProvider >
Next Steps
React Integration Learn more about React hooks
Queries Explore the query API