skip to content

Provider & Live Queries

SQLiteProvider opens a database once for the tree and runs onInit, useSQLiteContext hands it to screens, and change listeners drive live queries. Interviewers ask how components react to writes.

on this pageshow

explore

questions

6

In an Expo app, what do expo-sqlite's SQLiteProvider and useSQLiteContext do, and what renders before the database is ready?

level: juniorimportance: must knowfreq 45%

answer

  1. one database for a subtree
  2. context instead of prop drilling
  3. import, open, onInit, then children
  4. default renders null while loading
  5. hook throws outside the provider

basics

~20 s

SQLiteProvider opens one database for its subtree, importing an assetSource first and then running onInit, and puts it in React context; useSQLiteContext returns it in any descendant. By default the provider renders nothing until that work finishes.

solid answer

~40 s

`<SQLiteProvider databaseName="birds.db" onInit={migrate}>` opens the file once for everything below it: it imports a bundled `assetSource` if given, calls `openDatabaseAsync`, awaits `onInit`, and only then renders its children with the database in context. Any screen calls `const db = useSQLiteContext()` and gets that same `SQLiteDatabase`; outside a provider the hook throws `useSQLiteContext must be used within a <SQLiteProvider>`. In the default mode the provider renders `null` while it loads, so there is no spinner unless you use `useSuspense` with a Suspense fallback. Because children render only after `onInit`, no screen can query a half-migrated schema. Errors are rethrown during render unless you pass `onError`, and unmounting the provider closes the database.

code

tsx · 26 lines
tsx
import { SQLiteProvider, useSQLiteContext, type SQLiteDatabase } from 'expo-sqlite';
import { useEffect, useState } from 'react';
import { Text } from 'react-native';

async function migrate(db: SQLiteDatabase): Promise<void> {
  await db.execAsync(
    'CREATE TABLE IF NOT EXISTS sightings (id INTEGER PRIMARY KEY NOT NULL, species TEXT NOT NULL, seen_at INTEGER NOT NULL)'
  );
}

function SightingCount() {
  const db = useSQLiteContext();
  const [count, setCount] = useState(0);
  useEffect(() => {
    db.getFirstAsync<{ n: number }>('SELECT COUNT(*) AS n FROM sightings').then((r) => setCount(r?.n ?? 0));
  }, [db]);
  return <Text>{count} sightings logged</Text>;
}

export default function App() {
  return (
    <SQLiteProvider databaseName="birds.db" onInit={migrate}>
      <SightingCount />
    </SQLiteProvider>
  );
}

go deeper

for a junior

Recall the pair: SQLiteProvider opens and shares one database, useSQLiteContext reads it, and the hook throws outside a provider.

for a middle

Explain the setup order, import then open then onInit then children, and why rendering null until then means no screen can query an unmigrated schema.

for a senior

Place the provider at the root, decide how loading and setup errors are shown, and know it neither caches results nor refreshes screens on its own.

for a principal

Decide what sits on top of the provider, plain queries, a repository layer or an ORM, so screens stay independent of SQL as the app grows.

## What problem the provider solves An Expo app that stores data with `expo-sqlite` needs one open database object that many screens share: the sighting log, the sighting form, a statistics tab. Opening the file in every screen, or passing the database down as a prop through every layer, is repetitive and makes it easy for a screen to query before the schema is ready. `SQLiteProvider` is expo-sqlite's React component for this. It opens the database once, prepares it, and publishes it through React context. `useSQLiteContext()` is the hook that reads it back in any descendant. ## What the provider does, in order In expo-sqlite 57 the provider's setup runs these steps before rendering its children: 1. **Import** a bundled database file, if you passed `assetSource`. 2. **Open** the file named by `databaseName` (in `directory`, or the default database directory) with `openDatabaseAsync`, using the `options` prop as open options. 3. **Initialise** by awaiting `onInit(db)`, if you passed one. This is where the app's schema migrations go. 4. **Render** the children inside a context provider whose value is the open `SQLiteDatabase`. Props you meet in practice: | Prop | Purpose | |---|---| | `databaseName` | file name to open, required | | `directory` | where the file lives; defaults to expo-sqlite's database directory | | `options` | open options such as `enableChangeListener` | | `assetSource` | a bundled `.db` file to copy in first | | `onInit` | async setup run after opening, before children render | | `onError` | handler for setup errors; the default rethrows | | `useSuspense` | integrate with React Suspense instead of rendering `null` | ## What renders while it loads In the default (non-Suspense) mode, the provider keeps a loading flag in state and **returns `null`** until the database is open and `onInit` has resolved. The children are not mounted at all during that time, which has two consequences: - There is **no built-in loading UI.** For a spinner or skeleton, either keep the native splash screen up until the tree is ready, or switch to `useSuspense` and wrap the provider in a `Suspense` boundary with a `fallback`. - A child **can never run a query early.** `useSQLiteContext()` is only callable once the database exists, so no screen reads a schema that migrations have not finished. ## Reading the database in a screen ```tsx const db = useSQLiteContext(); const rows = await db.getAllAsync<Sighting>('SELECT * FROM sightings'); ``` The hook returns the same `SQLiteDatabase` object to every caller. It has no loading state of its own: it either returns the database or, when there is no `SQLiteProvider` above the component, **throws** an error saying it must be used within one. A component rendered in a different tree, such as a screen registered outside the provider, is the usual cause. ## Errors and teardown - If opening or `onInit` fails, the provider stores the error and, with no `onError`, **rethrows it during render**, so the nearest error boundary (or the development error screen) shows it. Passing `onError` lets you log it and render your own recovery. - When the provider unmounts, its effect cleanup calls `closeAsync()` on the database. Place the provider high in the tree, such as the root layout, so it lives as long as the app. ## One provider, or several A `SQLiteProvider` publishes exactly one database through one React context. Two consequences follow for apps with more than one file: - **Nesting shadows.** If a second provider is nested inside the first, `useSQLiteContext()` returns the **nearest** one. Components below the inner provider cannot reach the outer database through the hook. - **Side by side is fine.** Two providers in separate subtrees each serve their own children, as long as no screen needs both. For a screen that needs two files, open the second one yourself with `openDatabaseAsync` in a small module of your own, or keep one main database and attach the second with SQL. Most apps need only one provider at the root. ## Where the provider stops The provider is plumbing, not a data layer: - It does not re-run queries when data changes; live updates need the change listener (`enableChangeListener` plus `addDatabaseChangeListener`). - It does not cache query results; each `getAllAsync` reads the file again. - It does not decide your schema; `onInit` runs whatever migration code you give it.

  • How do you show a loading screen while SQLiteProvider opens the database?
    The default mode renders `null`, so either keep the native splash screen visible until the tree is ready, or pass `useSuspense` and wrap the provider in a React `Suspense` boundary whose `fallback` shows while the database opens and `onInit` runs.
  • What happens to the database when the SQLiteProvider unmounts?
    Its effect cleanup calls `closeAsync()` on the database it opened. That is why the provider belongs near the root of the app: mounting it inside a screen that comes and goes would close and reopen the database, and rerun `onInit`, on every visit.

saying these in an interview costs you the question

  • useSQLiteContext returns null outside a provider, so just null-check it
  • Children render immediately and can query while onInit is still running
  • SQLiteProvider shows a built-in loading spinner while it opens
  • The database stays open after its SQLiteProvider unmounts
  • The provider re-runs screen queries automatically when data changes
open as a page

In an Expo app, how do you make a bird-sighting log screen built on expo-sqlite update when a new sighting is saved on another screen?

level: middleimportance: must knowfreq 40%

basics

~10 s

Open the database with enableChangeListener: true, for example through SQLiteProvider's options, then subscribe with addDatabaseChangeListener and re-run the screen's query when an event names the sightings table. Remove the subscription on unmount.

open as a page

In an Expo app already using expo-sqlite, what do expo-sqlite/kv-store and the expo-sqlite/localStorage/install import provide, and what are their limits?

level: middleimportance: should knowfreq 28%

basics

~20 s

expo-sqlite/kv-store is an AsyncStorage-compatible key-value store kept in a SQLite table, with extra synchronous methods such as getItemSync. The localStorage/install import sets a synchronous globalThis.localStorage on native backed by the same store, and does nothing on web.

open as a page

In an Expo app, screens under an expo-sqlite SQLiteProvider go blank and lose their state whenever the parent re-renders; what is the likely cause and fix?

level: seniorimportance: should knowfreq 20%

basics

~20 s

An inline onInit arrow gives SQLiteProvider a new prop on every parent render; the provider then closes the database, renders null, reopens it and reruns onInit, remounting every child. Define onInit and options at module scope or memoize them.

open as a page

In expo-sqlite, what changes when you set SQLiteProvider's useSuspense prop, and how do you then handle a failure to open or initialise?

level: middleimportance: nice to knowfreq 14%

basics

~20 s

With useSuspense, SQLiteProvider suspends on the database promise, so the nearest React Suspense boundary shows its fallback until the database is open and onInit has run. onError cannot be combined with it; failures go to an error boundary instead.

open as a page

After a bulk import, an expo-sqlite live-query screen stutters and refetches thousands of times; how do change events behave, and how do you tame them?

level: seniorimportance: nice to knowfreq 14%

basics

~20 s

expo-sqlite's change listener fires once per changed row, as the row changes and before commit, for every database opened with enableChangeListener. A 5,000-row import means 5,000 events; coalesce them into one reload, ideally after the import finishes.

open as a page