Skip to content
Mobile
Skill

/expo-sqlite

Persist and query local data with expo-sqlite in Expo or React Native apps. Use for SQLite setup, SQL queries, schema migrations, transactions, SQLiteProvider, expo-sqlite/kv-store, or the localStorage polyfill.

From plugin
expo
52k5 skills
Install
$ npx -y skills add expo/expo --skill expo-sqlite --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/expo-sqlite

Context preview

The summary Claude sees to decide when to auto-load this skill.

Persist and query local data with expo-sqlite in Expo or React Native apps. Use for SQLite setup, SQL queries, schema migrations, transactions, SQLiteProvider, expo-sqlite/kv-store, or the localStorage polyfill.

SKILL.md

expo-sqlite.SKILL.md
name: expo-sqlite
description: Persist and query local data with expo-sqlite in Expo or React Native apps. Use for SQLite setup, SQL queries, schema migrations, transactions, SQLiteProvider, expo-sqlite/kv-store, or the localStorage polyfill.
license: MIT

expo-sqlite

Use SQLite for structured local data, offline queues, and persistent caches. For simple string key-value storage, use `expo-sqlite/kv-store`. Preserve an existing ORM or storage architecture unless the task calls for changing it.

Setup and API selection

Install with `npx expo install expo-sqlite`. Check the installed package version and its types before using newer APIs such as `db.sql`. Use the [SQLite documentation](https://docs.expo.dev/versions/latest/sdk/sqlite/) for the app's SDK version; use [unversioned docs](https://docs.expo.dev/versions/unversioned/sdk/sqlite/) only when working with unreleased code.

The package supports Android, iOS, macOS, tvOS, and web. Standard SQLite is included in Expo Go; custom native build options require rebuilding the app. Web requires additional setup in [references/web-and-config.md](references/web-and-config.md).

| Task | API | | ------------------------------------------------ | ------------------------------------- | | Static DDL or multiple SQL statements | `db.execAsync(source)` | | Write and receive `{ lastInsertRowId, changes }` | `db.runAsync(source, params)` | | Read one row, or `null` if absent | `db.getFirstAsync<T>(source, params)` | | Read a bounded result set | `db.getAllAsync<T>(source, params)` | | Iterate rows without collecting the full result | `db.getEachAsync<T>(source, params)` | | Concise parameterized queries | `` db.sql<T>`SELECT ...` `` | | Reuse a compiled statement | `db.prepareAsync(source)` |

Prefer asynchronous methods for application work. Synchronous database methods can block the JavaScript thread, especially for large queries or migrations.

Bind values, not SQL syntax

`execAsync()` does not bind or escape values. Use it for trusted, static SQL. For data, use variadic arguments, arrays, or named parameters:

import type { SQLiteDatabase } from 'expo-sqlite';

async function addTodo(db: SQLiteDatabase, title: string) {
  const result = await db.runAsync('INSERT INTO todos (title) VALUES (?)', title);
  return result.lastInsertRowId;
}

async function setCompleted(db: SQLiteDatabase, id: number, completed: boolean) {
  await db.runAsync('UPDATE todos SET completed = $completed WHERE id = $id', {
    $completed: completed ? 1 : 0,
    $id: id,
  });
}

Bindings represent values, not table names, column names, or SQL fragments. Use fixed SQL or an allowlist for dynamic identifiers. Bind `null` for SQL NULL; serialize objects explicitly. `Uint8Array` and `ArrayBuffer` can be bound to BLOB columns in the current package.

The `db.sql` tag replaces interpolations with bound parameters. Do not put quotes around `${value}` or interpolate an array expecting an expanded `IN` list:

import type { SQLiteDatabase } from 'expo-sqlite';

type Todo = { id: number; title: string; completed: number };

async function findTodo(db: SQLiteDatabase, id: number) {
  const sql = db.sql;
  return await sql<Todo>`SELECT * FROM todos WHERE id = ${id}`.first();
}

Awaiting a tagged `SELECT` returns rows; `.first()` returns a row or `null`, `.each()` iterates rows, and `.values()` returns arrays of column values. Writes without `RETURNING` normally return `SQLiteRunResult`; prefer `runAsync()` when you need unambiguous write metadata. A generic type describes the expected row shape; it does not validate stored data or transform SQLite integers into JavaScript booleans.

React integration and migrations

Mount one stable `SQLiteProvider` above the consumers of a database. Descendants call `useSQLiteContext()` to obtain it. Put initialization in `onInit`, which completes before the children render. Keep `onInit` and options stable so a render does not reopen the database.

This example migrates a new database through versions 1 and 2, and upgrades an existing version 1 database without deleting its rows:

import { SQLiteProvider, type SQLiteDatabase } from 'expo-sqlite';
import type { PropsWithChildren } from 'react';

export function DatabaseProvider({ children }: PropsWithChildren) {
  return (
    <SQLiteProvider databaseName="app.db" onInit={initializeDatabase}>
      {children}
    </SQLiteProvider>
  );
}

async function initializeDatabase(db: SQLiteDatabase) {
  // Connection settings must be applied outside the migration transaction.
  await db.execAsync('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;');

  const version = await db.getFirstAsync<{ user_version: number }>('PRAGMA user_version');
  if (version === null) {
    throw new Error('Could not read the database schema version');
  }
  if (version.user_version > 2) {
    throw new Error('Database schema is newer than this app supports');
  }
  if (version.user_version === 2) {
    return;
  }

  // onInit gates consumers; no other database work should run during initialization.
  await db.withTransactionAsync(async () => {
    if (version.user_version < 1) {
      await db.execAsync(`
        CREATE TABLE todos (id INTEGER PRIMARY KEY NOT NULL, title TEXT NOT NULL);
      `);
    }
    if (version.user_version < 2) {
      await db.execAsync('ALTER TABLE todos ADD COLUMN completed INTEGER NOT NULL DEFAULT 0');
    }
    await db.execAsync('PRAGMA user_version = 2');
  });
}

Keep schema changes and their `user_version` update in the same transaction so failure rolls both back. WAL generally improves database performance; enable foreign keys on each connection that relies on them. Do not delete an existing database to work around a migration e

Read more
Ships withexpo

An open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.

Get the whole plugin
Stats
52,284
Stars
14,007
Forks
Active
Maintenance
TypeScript
Language
MIT
License
1h ago
Last commit
10y ago
Created
15d ago
Added

Repo: expo/expo

Other skills on expo.