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?
answer
- a storage table in its own file
- AsyncStorage method names
- extra Sync methods
- strings only
- install is a no-op on web
basics
~20 sexpo-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 sThe 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 linesimport 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
Recall that kv-store keeps string key-value pairs in SQLite with AsyncStorage-style methods, plus synchronous variants like getItemSync.
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.
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.
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