React Native SQLite Database
AsyncStorage works well for small values, but it struggles with thousands of records and with questions such as "show unpaid invoices from last month." SQLite, a compact database engine built into every phone, stores structured tables on the device and answers such questions quickly, even without internet access.
When SQLite Fits
Need Choice ───────────────────────────────────────────── Theme, language, small flags AsyncStorage Login token SecureStore Thousands of related records SQLite Search, sort, filter offline SQLite Data shared across devices Cloud database
Tables, Rows, and Columns
A table resembles a spreadsheet. Columns define the fields, and each row holds one record.
Table: tasks ┌────┬──────────────┬───────┬────────────┐ │ id │ title │ done │ created │ ├────┼──────────────┼───────┼────────────┤ │ 1 │ Buy milk │ 0 │ 2026-11-01 │ │ 2 │ Write report │ 1 │ 2026-11-02 │ │ 3 │ Call Ravi │ 0 │ 2026-11-03 │ └────┴──────────────┴───────┴────────────┘
Install
npx expo install expo-sqlite
Open the Database and Create a Table
import * as SQLite from 'expo-sqlite';
const db = await SQLite.openDatabaseAsync('app.db');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER DEFAULT 0,
created TEXT
);
`);
The file lives on the phone. IF NOT EXISTS makes the command safe to run at every startup.
Insert Rows
Pass values as separate parameters. The question marks act as placeholders, which blocks SQL injection, an attack where crafted text changes the meaning of a command.
await db.runAsync(
'INSERT INTO tasks (title, done, created) VALUES (?, ?, ?)',
['Buy milk', 0, new Date().toISOString()]
);
Read Rows
const all = await db.getAllAsync('SELECT * FROM tasks ORDER BY id DESC');
const pending = await db.getAllAsync('SELECT * FROM tasks WHERE done = ?', [0]);
const one = await db.getFirstAsync('SELECT * FROM tasks WHERE id = ?', [1]);
Search by Text
const results = await db.getAllAsync(
'SELECT * FROM tasks WHERE title LIKE ?',
[`%${searchText}%`]
);
Update and Delete
await db.runAsync('UPDATE tasks SET done = 1 WHERE id = ?', [2]);
await db.runAsync('DELETE FROM tasks WHERE id = ?', [3]);
Wrap It in a Service File
Keep database commands out of screens. Create small functions that screens call.
// src/db/taskService.js
export const addTask = (db, title) =>
db.runAsync('INSERT INTO tasks (title, created) VALUES (?, ?)',
[title, new Date().toISOString()]);
export const getTasks = (db) =>
db.getAllAsync('SELECT * FROM tasks ORDER BY id DESC');
export const toggleTask = (db, id, done) =>
db.runAsync('UPDATE tasks SET done = ? WHERE id = ?', [done ? 1 : 0, id]);
Use It in a Screen
import { SQLiteProvider, useSQLiteContext } from 'expo-sqlite';
function TaskScreen() {
const db = useSQLiteContext();
const [tasks, setTasks] = useState([]);
const load = async () => setTasks(await getTasks(db));
useEffect(() => { load(); }, []);
return (
<FlatList
data={tasks}
keyExtractor={(t) => String(t.id)}
renderItem={({ item }) => (
<TouchableOpacity onPress={async () => { await toggleTask(db, item.id, !item.done); load(); }}>
<Text style={{ textDecorationLine: item.done ? 'line-through' : 'none' }}>
{item.title}
</Text>
</TouchableOpacity>
)}
/>
);
}
Related Tables
Split data to avoid repetition. A categories table holds names once, and each task stores a category_id. A JOIN query combines them.
SELECT tasks.title, categories.name
FROM tasks
JOIN categories ON tasks.category_id = categories.id;
tasks ──category_id──► categories many one
Migrations
Updated apps sometimes need new columns. Track a schema version number, compare it at startup, and run the missing upgrade commands in order. Existing user data stays safe.
Habits That Prevent Trouble
- Always use placeholders for user values
- Add indexes to columns you search often
- Run several related writes inside a transaction
- Avoid storing secrets in plain database files
- Test on a device with thousands of rows
Summary
SQLite stores structured data on the device with fast search, sorting, and filtering. Create tables at startup, use parameter placeholders for every command, and keep queries in service files. Plan migrations early so app updates preserve user data.
