deep-code-review
In-depth design-focused code review - understands codebase context before evaluating PR changes, posts structured feedback to GitHub
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.
$ npx -y skills add expo/expo --skill expo-sqlite --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/expo-sqliteContext 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.
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
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.
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.
`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.
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
An open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.
Repo: expo/expo
In-depth design-focused code review - understands codebase context before evaluating PR changes, posts structured feedback to GitHub
Write TSDoc comments for Expo SDK APIs following official conventions. MUST USE when introducing new user-facing TypeScript APIs in expo-* packages - document…
Run Expo's configured AI code reviewer on local changes or an expo/expo pull request, summarize findings and reviewer coverage, retain PR previews by default…
Test Expo Router features on Android emulators using ADB. Use after implementing native Android features or when verifying UI behavior on Android.