---
title: Modern SQLite for React Native apps
authors: Simon Grimm
published: January 7, 2025
categories: Development
tags: sqlite, drizzle
---

_This is a guest post from Simon Grimm, creator of [Galaxies.dev](http://galaxies.dev/), where Simon helps developers learn React Native through fast-paced courses and personal support._

…

Working with databases in mobile apps has always been a challenge. While SQLite is a reliable and lightweight option, its raw interface can feel outdated and cumbersome, especially in modern React Native workflows. Writing raw SQL queries and managing schemas manually is error-prone and time-consuming, leaving many developers searching for a better solution.

That’s where [Expo SQLite](https://docs.expo.dev/versions/latest/sdk/sqlite/) and [Drizzle](https://orm.drizzle.team/) come in.

By combining the flexibility of SQLite with the type safety and developer-friendly abstractions of Drizzle, you can drastically simplify your database management process without sacrificing performance or control.

In this blog post, we’ll walk you through the essential steps to integrate Expo SQLite and Drizzle into your React Native project, use migrations and queries, and even debug your app with a special Expo Dev Tool Plugin!

## Setting Up Expo SQLite and Drizzle

Getting started with Expo SQLite and Drizzle is surprisingly straightforward.

After [creating your Expo project](https://docs.expo.dev/get-started/create-a-project/), you can install the required packages and dependencies:

```bash
npx expo install expo-sqlite

npm i -D drizzle-kit
npm i drizzle-orm babel-plugin-inline-import
npm i expo-drizzle-studio-plugin
```

While `drizzle-orm` is the main package, the other packages will help us to generate **migrations**, import SQL files and finally help us debug our database.

As [described by Drizzle](https://orm.drizzle.team/docs/connect-expo-sqlite), you need to customize the `metro.config.js` and `babel.config.js` in order to load SQL files later.

Once that’s done, create a `drizzle.config.ts` at the root of your project that looks like this:

```typescript
import type { Config } from 'drizzle-kit';

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

Now you can write your database schema in a new `/db/schema.ts` file and generate migrations with a script.

For example, you could define two tables with a foreign key relation:

```typescript
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(),
});

// Export Task to use as an interface in your app
export type Task = typeof tasks.$inferSelect;
```

See how this is a lot cleaner, safer, and simpler than pure SQL statements?

With the previously installed `drizzle-kit` we can now generate our migrations files by running:

```bash
npx drizzle-kit generate
```

This will generate some files in a `/drizzle` folder, or the output you defined in the config before.

Now we can wrap the `SQLiteProvider` from `expo-sqlite` around our app in the `_layout.tsx`, and we can even use **Suspense** to make sure our database is ready before the rest of our app loads!

```typescript
import { Stack } from 'expo-router';
import { Suspense } from 'react';
import { ActivityIndicator } from 'react-native';
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 (
    <Suspense fallback={<ActivityIndicator size="large" />}>
      <SQLiteProvider
        databaseName={DATABASE_NAME}
        options={{ enableChangeListener: true }}
        useSuspense>
        <Stack>
          <Stack.Screen name="index" options={{ title: 'Tasks' }} />
        </Stack>
      </SQLiteProvider>
    </Suspense>
  );
}
```

Applying our migrations is now as easy as calling `useMigrations`, and your SQLite database is set up and initialized!

## Type-Safe and Clean Queries

Creating a database schema already felt satisfying, but wait until you see the queries! This is where Drizzle really shines, and it works right out of the box with Expo.

All you have to do is access your database (which you can easily do with the [useSQLiteContext hook](https://docs.expo.dev/versions/latest/sdk/sqlite/#usesqlitecontext-hook)!), and then pass it to Drizzle like this in your component:

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

export default function Index() {
  const db = useSQLiteContext();
  const drizzleDb = drizzle(db, { schema});

  // more code 
}
```

> Note: I added the schema of our database as second param here because I previously encountered some typing issues. It certainly won’t hurt!

At this point, you can [query your data with Drizzle](https://orm.drizzle.team/docs/data-querying) in different ways. Let’s take a look at some examples.

The first way would be to use a query builder, which feels _almost_ like writing SQL, but in a type-safe way:

```typescript
// Query data
const result = await drizzleDb.select().from(tasks);

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

While typing, you will notice that you actually work with our schema definition for `tasks` from before, so there are no table names or strings in here!

You can also write these as easier [Drizzle Queries](https://orm.drizzle.team/docs/rqb), which are an extension to your schema and the query builder:

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

If all of that feels too fancy for some of your teammates, there’s always the escape hatch for using raw SQL statements as well:

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

It may look strange but [magical sql operator](https://orm.drizzle.team/docs/sql) works just like that!

But hold on, we haven’t even talked about the icing on the cake…

### Live Queries!

There is another Expo specific hook called `useLiveQuery` which will automatically update when your underlying data in SQLite changes.

We already prepared our Expo SQLite provider for this by passing the `enableChangeListener` to it initially, and now we can use the hook in our component like this:

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

If you have worked with SQLite in the past and jumped through all the hoops to keep your data updated and in sync, this feels like a fresh wind on a spring morning.

While these are some of the most helpful additions, Drizzle comes with many more features so it’s worth [going through their docs](https://orm.drizzle.team/docs/rqb) to read about all the functions and utils!

## Debugging SQLite

No post about SQLite would be complete without talking about debugging which, to be honest, has historically been an absolute pain in the backside.

From using shell commands to figure out the location of the SQLite file on my Mac...to adding a share plugin...and sending a WhatsApp message from my device...I’ve seen everything.

**Those days are over.**

The way to debug your Expo SQLite database with Drizzle is to use the [Drizzle Studio Expo dev tool plugin](https://github.com/drizzle-team/drizzle-studio-expo), which is just a simple hook you can add to your component:

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

export default function Index() {
  const db = useSQLiteContext();
  const drizzleDb = drizzle(db);
  useDrizzleStudio(db);

  // more code 
}
```

Now when you start your app, simply press `shift + m` in your terminal and select the `expo-drizzle-studio-plugin`, which will bring up a new browser window that shows [Drizzle Studio](https://orm.drizzle.team/drizzle-studio/overview), directly connected to your SQLite database!

![Drizzle Studio in the browser for working on your SQLite database](https://cdn.sanity.io/images/9r24npb8/production/a9f542e27f4a9faab33aecc1ef6d91b45feb96aa-1556x841.png)

From here, you can directly update your data, add and remove records, or run any arbitrary SQL commands with the SQL runner. You can even export all your data to JSON or CSV.   
  
Compared to everything I’ve used before, this is the easiest way to debug your React Native SQLite database.

## Wrapping Up

Expo SQLite and Drizzle can make working with SQLite in React Native cleaner, simpler, and much more enjoyable.

From setting up the database to writing type-safe queries, this combo is a great way to modernize your data workflow.

If you found this quick introduction helpful, explore a full [project course about Expo SQLite and data management on Galaxies.dev](https://galaxies.dev/project/handyman-reports/1-1), my online school focused 100% on React Native.

PS: I also showcased the **SQLite live queries** on my YouTube channel 👇

[Video: Modern SQLite video demo](https://www.youtube.com/watch?v=AT5asDD3u_A)