Using SQLite in React Native: A Modern Developer's Guide

Build robust offline-first mobile applications with type-safe database operations using Expo SQLite and Drizzle ORM.

Why SQLite Matters for React Native

SQLite has been a cornerstone of mobile data persistence for nearly two decades. Its serverless, self-contained architecture makes it ideal for mobile environments where network connectivity cannot be guaranteed. Unlike key-value stores like AsyncStorage, SQLite provides full SQL querying capabilities, relational data modeling, and transaction support--essential features for applications handling complex data relationships.

The case for local databases in React Native extends beyond simple persistence. Applications requiring offline functionality--field service apps, inventory management systems, productivity tools--depend on reliable local storage. When users are in areas with poor connectivity or need instant data access, SQLite provides the foundation for seamless offline experiences. Our web development team specializes in building offline-first mobile applications that deliver reliable performance regardless of network conditions.

Modern ORMs like Drizzle have transformed SQLite integration from raw SQL manipulation to type-safe, developer-friendly operations that feel native to the JavaScript ecosystem.

Setting Up Your Development Environment

Getting started with SQLite in React Native requires choosing between several libraries, each with distinct advantages. The Expo ecosystem offers expo-sqlite as the official solution, which provides a straightforward API and integrates seamlessly with Expo's tooling.

Installation

npx expo install expo-sqlite
npm i -D drizzle-kit
npm i drizzle-orm babel-plugin-inline-import
npm i expo-drizzle-studio-plugin

This installation sequence brings in the core SQLite module, Drizzle ORM for type-safe database operations, Babel support for inline SQL imports, and a development tool for database debugging.

Configuration

Configuration requires updating both metro.config.js and babel.config.js to enable SQL file imports. The Babel plugin babel-plugin-inline-import allows embedding raw SQL statements directly in your codebase.

Defining Your Database Schema

Schema definition with Drizzle ORM brings TypeScript's type safety to database design. Rather than writing raw SQL CREATE TABLE statements, you define tables using a declarative syntax that generates types automatically.

Schema Definition Example

import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

export const tasks = sqliteTable('tasks', {
 id: integer('id').primaryKey({ autoIncrement: true }),
 name: text('name').notNull(),
 list_id: integer('list_id')
 .notNull()
 .references(() => lists.id),
});

export const lists = sqliteTable('lists', {
 id: integer('id').primaryKey({ autoIncrement: true }),
 name: text('name').notNull(),
});

This schema defines two tables with a foreign key relationship. The type safety extends throughout your application; Drizzle generates inference types that ensure your queries and data transformations align with your schema.

Migration Configuration

import type { Config } from 'drizzle-Kit';

export default {
 schema: './db/schema.ts',
 out: './drizzle',
 dialect: 'sqlite',
 driver: 'expo',
} satisfies Config;

Database Provider Setup

Integrating the database into your React Native application requires wrapping your app with the SQLite provider. This provider manages database connections, handles migrations, and exposes the database context.

import { SQLiteProvider, openDatabaseSync } from 'expo-sqlite';
import { drizzle } from 'drizzle-orm/expo-sqlite';
import { useMigrations } from 'drizzle-orm/expo-sqlite/migrator';
import migrations from '@/drizzle/migrations';

export const DATABASE_NAME = 'tasks';

export default function RootLayout() {
 const expoDb = openDatabaseSync(DATABASE_NAME);
 const db = drizzle(expoDb);
 const { success, error } = useMigrations(db, migrations);

 return (
 <SQLiteProvider
 databaseName={DATABASE_NAME}
 options={{ enableChangeListener: true }}
 useSuspense>
 {/* Your app components */}
 </SQLiteProvider>
 );
}

The enableChangeListener option activates real-time update notifications, which Drizzle leverages for its live query functionality.

Performing CRUD Operations

With your database configured, executing operations becomes straightforward. Drizzle provides multiple query interfaces--from low-level SQL execution to high-level query builders.

Query Builder Approach

import { useSQLiteContext } from 'expo-sqlite';
import { drizzle } from 'drizzle-orm/expo-sqlite';
import * as schema from '@/db/schema';

const db = useSQLiteContext();
const drizzleDb = drizzle(db, { schema });

// Select all tasks
const result = await drizzleDb.select().from(tasks);

// Insert a new task
await drizzleDb.insert(tasks).values({
 name: `Task ${Math.floor(Math.random() * 1000)}`,
 list_id: 1,
});

Complex Queries with Relationships

const result = await drizzleDb.query.tasks.findMany({
 with: {
 lists: true,
 },
});

Raw SQL When Needed

import { sql } from 'drizzle-orm';

await db.execute(
 sql`select * from tasks where ${tasks.list_id} = ${id}`
);

Live Queries for Reactive Data

One of Expo SQLite's most powerful features is its change notification system. When combined with Drizzle, this enables reactive components that update automatically when underlying data changes.

import { useLiveQuery } from 'drizzle-orm/expo-sqlite';

const { data } = useLiveQuery(
 drizzleDb.select().from(tasks).leftJoin(lists, eq(tasks.list_id, lists.id))
);

This pattern eliminates manual data synchronization code. Rather than implementing observers or polling mechanisms, you declare dependencies through queries and let the framework handle updates.

Performance Optimization Strategies

Database performance in mobile environments demands intentional optimization.

Use Transactions

await db.transaction(async (tx) => {
 for (const item of items) {
 await tx.insert(tasks).values(item);
 }
});

Transactions are essential for write-heavy operations. Grouping multiple inserts into a single transaction reduces execution time significantly compared to individual commits.

Key Optimization Techniques

TechniqueBenefit
TransactionsOrder of magnitude faster for bulk writes
WAL ModeImproved concurrent read/write performance
Prepared StatementsReduced query parsing overhead
Proper IndexingDramatic query speedup for large datasets

Performance benchmarks show that using transactions and batching queries makes a significant difference, especially on Android devices. Write-Ahead Logging (WAL) mode provides advantages for concurrent access scenarios.

Debugging Your Database

The Drizzle Studio Expo plugin provides a browser-based interface connected directly to your running application:

import { useDrizzleStudio } from 'expo-drizzle-studio-plugin';

export default function Index() {
 const db = useSQLiteContext();
 const drizzleDb = drizzle(db);
 useDrizzleStudio(db);
 // Your component logic
}

After adding the hook, pressing shift + m in your development terminal and selecting the plugin opens Drizzle Studio in your browser. From this interface, you can:

  • Browse tables and their contents
  • Run SQL queries directly
  • Modify data with visual editors
  • Export results to JSON or CSV
  • Inspect migration status

This debugging workflow dramatically accelerates development by providing immediate visibility into database state.

Best Practices and Recommendations

Do

  • Separate schema definition into dedicated files for maintainability
  • Use migrations for all schema changes with proper versioning
  • Handle migration failures gracefully with error boundaries
  • Prefer type-safe query builders over raw SQL when possible
  • Test database operations using in-memory SQLite during unit testing

Don't

  • Modify tables manually in production without migrations
  • Execute individual inserts in loops without transactions
  • Ignore migration failures during app upgrades
  • Use raw SQL when query builders provide equivalent functionality

Successful SQLite integration in React Native requires adherence to these practices that prevent common pitfalls and ensure reliable data management.

Conclusion

SQLite remains the foundation of mobile data persistence, and modern tooling has made its integration with React Native both straightforward and type-safe. By combining Expo SQLite with Drizzle ORM, developers gain the benefits of a battle-tested embedded database with developer experience rivaling cloud-first solutions.

For React Native developers building applications that require reliable local storage, this modern SQLite stack provides a compelling foundation that balances simplicity with power. Whether you're building a mobile application for field service teams or a consumer app that works offline, proper local data management is essential for user satisfaction and app performance.

Looking to integrate AI capabilities into your offline-first mobile application? Our AI automation services can help you add intelligent features that work seamlessly with local data storage.

Frequently Asked Questions

Ready to Build Offline-First Mobile Apps?

Our team specializes in React Native development with modern data persistence patterns.

Sources

  1. LogRocket: Using SQLite with React Native - Comprehensive tutorial using react-native-sqlite-storage
  2. Expo Blog: Modern SQLite for React Native apps - Modern approach using Expo SQLite with Drizzle ORM
  3. PowerSync: React Native Database Performance Comparison - Performance benchmarks and optimization techniques
  4. Drizzle ORM: Connect with Expo SQLite - Type-safe database operations guide
  5. Drizzle Studio Expo Plugin - Debugging tool for React Native SQLite