skip to content

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%

answer

  1. a storage table in its own file
  2. AsyncStorage method names
  3. extra Sync methods
  4. strings only
  5. install is a no-op on web

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.

solid answer

~40 s

The default export of `expo-sqlite/kv-store` is a `SQLiteStorage` instance on its own database file, `ExpoSQLiteStorage`, with a `storage` table of text keys and values. It exposes the `@react-native-async-storage/async-storage` method names (`getItem`, `setItem`, `removeItem`, `getAllKeys`, `clear`, `mergeItem`, `multiGet`, `multiSet`), so switching is an import change, plus `*Async` and synchronous `*Sync` variants such as `getItemSync`. `setItem` also accepts an updater function, applied as a read-modify-write in one transaction. Values must be strings: anything else throws, so `JSON.stringify` objects. `import 'expo-sqlite/localStorage/install'` installs `globalThis.localStorage` on native, backed by the same default store through its synchronous methods, and is a no-op on web where the browser's own exists. Limits: the file is not encrypted, so no secrets; sync calls block the JS thread; and it is for small settings, not the sightings themselves.

code

typescript · 20 lines
typescript
import Storage from 'expo-sqlite/kv-store';
import 'expo-sqlite/localStorage/install';

type MapRegion = { latitude: number; longitude: number; zoom: number };

export function saveLastRegion(region: MapRegion): void {
  Storage.setItemSync('lastRegion', JSON.stringify(region));
}

export function readLastRegion(): MapRegion | null {
  const raw = Storage.getItemSync('lastRegion');
  return raw ? (JSON.parse(raw) as MapRegion) : null;
}

export async function countAppOpen(): Promise<void> {
  await Storage.setItem('opens', (prev) => String(Number(prev ?? '0') + 1));
}

// Same default store, web-style API, shared with web code:
export const units = (): string => globalThis.localStorage.getItem('units') ?? 'metric';

go deeper

for a junior

Recall that kv-store keeps string key-value pairs in SQLite with AsyncStorage-style methods, plus synchronous variants like getItemSync.

for a middle

Explain the strings-only rule, the atomic updater form of setItem, and that localStorage/install shares the default store and is a no-op on web.

for a senior

Draw the boundaries: no secrets, no main data in JSON blobs, sync calls only for tiny startup values, and a separate file with its own lifecycle.

for a principal

Decide on one storage story for settings, structured data and secrets across native and web, instead of letting each feature pick a library.

## Why a key-value store inside expo-sqlite Most apps need a few small, unstructured values next to their real data: the last map region the user looked at, a chosen unit system, whether the onboarding tip was dismissed. A bird-sighting app that already depends on `expo-sqlite` can keep those in the same library instead of adding another storage dependency. That is what `expo-sqlite/kv-store` is for. ## What kv-store is - The module's default export (also exported as `Storage` and `AsyncStorage`) is an instance of the `SQLiteStorage` class. - That instance uses its **own database file**, named `ExpoSQLiteStorage`, separate from `birds.db`. - Inside it, a single `storage` table holds `key TEXT PRIMARY KEY` and `value TEXT`, created and versioned by the module's own small migration. - You can create other stores with `new SQLiteStorage('another-file')` when you want settings in a different file. ## The API surface The expo-sqlite docs describe it as a drop-in replacement for `@react-native-async-storage/async-storage`: the same method names, so an existing import can be swapped. | Family | Methods | Notes | |---|---|---| | AsyncStorage-compatible | `getItem`, `setItem`, `removeItem`, `getAllKeys`, `clear`, `mergeItem`, `multiGet`, `multiSet`, `multiRemove`, `multiMerge` | return promises | | Explicit async | `getItemAsync`, `setItemAsync`, `removeItemAsync`, `getAllKeysAsync`, `clearAsync` | same behaviour | | Synchronous | `getItemSync`, `setItemSync`, `removeItemSync`, `getAllKeysSync`, `clearSync` | block the JS thread | Details worth knowing in an interview: - **Strings only.** A non-string key or value throws an error telling you to use a string; nothing is coerced. Store objects with `JSON.stringify` and read them with `JSON.parse`. - **Updater functions.** `setItem(key, (prev) => next)` reads the previous value and writes the new one atomically; the async version runs inside an exclusive transaction, so two concurrent increments do not lose an update. - **`mergeItem`** deep-merges JSON objects stored as strings, using the same updater mechanism. - **Synchronous reads** are the headline convenience: reading a theme before the first render without an await. They run on the JavaScript thread, so keep them for tiny values. ## The localStorage install `import 'expo-sqlite/localStorage/install'` puts a Web Storage-shaped object at `globalThis.localStorage` on Android and iOS: - it is backed by the **same default kv-store instance**, so `localStorage.getItem('units')` and `Storage.getItemSync('units')` read the same row; - every method is **synchronous**, matching the web API (`getItem`, `setItem`, `removeItem`, `clear`, `key`, `length`); - `setItem` converts the value with `String(value)`, as browsers do, unlike kv-store's own methods, which reject non-strings; - on **web** the import is a no-op and is excluded from the production bundle, because the browser already has `localStorage`. The main reason to use it is shared code: a settings module written against `localStorage` runs on web and native unchanged. ## Switching from AsyncStorage keeps the API, not the data Changing `import AsyncStorage from '@react-native-async-storage/async-storage'` to `import AsyncStorage from 'expo-sqlite/kv-store'` is enough for the code to compile and run, because the method names match. It does **not** move existing users' values: the old library stores them in its own location, and kv-store starts empty in `ExpoSQLiteStorage`. For an app already in users' hands, ship a one-off copy: 1. On launch, check a flag key in kv-store. 2. If missing, read every key from the old library and write them with kv-store's `multiSet`. 3. Set the flag, and remove the old library in a later release. ## Limits and when not to use it 1. **Not a secret store.** The file is a plain SQLite database in the app's data directory. Tokens and credentials belong in the platform's secure storage. 2. **Not the main data model.** Sightings with species, time and location belong in real tables in `birds.db`, where they can be indexed, queried and migrated. A JSON blob under one key cannot. 3. **Synchronous calls block.** `getItemSync` and `localStorage` are fine for a handful of small values at startup; large values or loops of sync calls stall rendering and gestures. 4. **Separate file, separate lifecycle.** Clearing kv-store does not touch `birds.db`, and your migrations do not touch kv-store.

  • How does setItem with an updater function avoid losing a concurrent increment?
    The async path runs the read of the previous value, your function and the write inside `withExclusiveTransactionAsync`, so no other write to that store can interleave between the read and the write. Two increments therefore apply one after the other instead of both reading the same old value.
  • Why would you pick expo-sqlite/kv-store over a separate key-value library in an Expo app?
    If `expo-sqlite` is already a dependency, kv-store adds no native module, keeps the AsyncStorage method names so existing code ports with an import change, and adds synchronous reads. A dedicated library can still win where its specific features matter.

saying these in an interview costs you the question

  • kv-store values are stored inside the app's own main database file
  • setItem accepts numbers and objects and converts them for you
  • kv-store is safe for auth tokens because it lives in SQLite
  • The localStorage install replaces the browser's localStorage on web
  • Synchronous kv-store reads are free because SQLite is local