Skip to main content

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

1
Install Zero and dependencies
2
npm
npm install @rocicorp/zero @rocicorp/zero-react-native expo-sqlite
yarn
yarn add @rocicorp/zero @rocicorp/zero-react-native expo-sqlite
pnpm
pnpm add @rocicorp/zero @rocicorp/zero-react-native expo-sqlite
3
Configure Expo SQLite
4
For Expo projects, ensure expo-sqlite is properly configured:
5
npx expo install expo-sqlite
6
Update app.json (Expo)
7
Add SQLite plugin to your app.json:
8
{
  "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>
  );
}

Performance Optimization

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>;
}

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;
}

Platform-Specific Considerations

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

Build docs developers (and LLMs) love