expo-sqlite
Version:
Provides access to a database using SQLite (https://www.sqlite.org/). The database is persisted across restarts of your app.
209 lines • 7.07 kB
JavaScript
import { Asset } from 'expo-asset';
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
import ExpoSQLite from './ExpoSQLite';
import { openDatabaseAsync } from './SQLiteDatabase';
import { createDatabasePath } from './pathUtils';
/**
* Create a context for the SQLite database
*/
const SQLiteContext = createContext(null);
/**
* Context.Provider component that provides a SQLite database to all children.
* All descendants of this component will be able to access the database using the [`useSQLiteContext`](#usesqlitecontext) hook.
*/
export function SQLiteProvider({ children, onError, useSuspense = false, ...props }) {
if (onError != null && useSuspense) {
throw new Error('Cannot use `onError` with `useSuspense`, use error boundaries instead.');
}
if (useSuspense) {
return <SQLiteProviderSuspense {...props}>{children}</SQLiteProviderSuspense>;
}
return (<SQLiteProviderNonSuspense {...props} onError={onError}>
{children}
</SQLiteProviderNonSuspense>);
}
/**
* A global hook for accessing the SQLite database across components.
* This hook should only be used within a [`<SQLiteProvider>`](#sqliteprovider) component.
*
* @example
* ```tsx
* export default function App() {
* return (
* <SQLiteProvider databaseName="test.db">
* <Main />
* </SQLiteProvider>
* );
* }
*
* export function Main() {
* const db = useSQLiteContext();
* console.log('sqlite version', db.getFirstSync('SELECT sqlite_version()'));
* return <View />
* }
* ```
*/
export function useSQLiteContext() {
const context = useContext(SQLiteContext);
if (context == null) {
throw new Error('useSQLiteContext must be used within a <SQLiteProvider>');
}
return context;
}
let databaseInstance = null;
function SQLiteProviderSuspense({ databaseName, directory, options, assetSource, children, onInit, }) {
const databasePromise = getDatabaseAsync({
databaseName,
directory,
options,
assetSource,
onInit,
});
const database = use(databasePromise);
return <SQLiteContext.Provider value={database}>{children}</SQLiteContext.Provider>;
}
function SQLiteProviderNonSuspense({ databaseName, directory, options, assetSource, children, onInit, onError, }) {
const databaseRef = useRef(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function setup() {
try {
const db = await openDatabaseWithInitAsync({
databaseName,
directory,
options,
assetSource,
onInit,
});
databaseRef.current = db;
setLoading(false);
}
catch (e) {
setError(e);
}
}
async function teardown(db) {
try {
await db?.closeAsync();
}
catch (e) {
setError(e);
}
}
setup();
return () => {
const db = databaseRef.current;
teardown(db);
databaseRef.current = null;
setLoading(true);
};
}, [databaseName, directory, options, onInit]);
if (error != null) {
const handler = onError ??
((e) => {
throw e;
});
handler(error);
}
if (loading || !databaseRef.current) {
return null;
}
return <SQLiteContext.Provider value={databaseRef.current}>{children}</SQLiteContext.Provider>;
}
function getDatabaseAsync({ databaseName, directory, options, assetSource, onInit, }) {
if (databaseInstance?.promise != null &&
databaseInstance?.databaseName === databaseName &&
databaseInstance?.directory === directory &&
databaseInstance?.options === options &&
databaseInstance?.onInit === onInit) {
return databaseInstance.promise;
}
let promise;
if (databaseInstance?.promise != null) {
promise = databaseInstance.promise
.then((db) => {
db.closeAsync();
})
.then(() => {
return openDatabaseWithInitAsync({
databaseName,
directory,
options,
assetSource,
onInit,
});
});
}
else {
promise = openDatabaseWithInitAsync({ databaseName, directory, options, assetSource, onInit });
}
databaseInstance = {
databaseName,
directory,
options,
onInit,
promise,
};
return promise;
}
async function openDatabaseWithInitAsync({ databaseName, directory, options, assetSource, onInit, }) {
if (assetSource != null) {
await importDatabaseFromAssetAsync(databaseName, assetSource, directory);
}
const database = await openDatabaseAsync(databaseName, options, directory);
if (onInit != null) {
await onInit(database);
}
return database;
}
/**
* Imports an asset database into the SQLite database directory.
*
* Exposed only for testing purposes.
* @hidden
*/
export async function importDatabaseFromAssetAsync(databaseName, assetSource, directory) {
const asset = await Asset.fromModule(assetSource.assetId).downloadAsync();
if (!asset.localUri) {
throw new Error(`Unable to get the localUri from asset ${assetSource.assetId}`);
}
const path = createDatabasePath(databaseName, directory);
await ExpoSQLite.importAssetDatabaseAsync(path, asset.localUri, assetSource.forceOverwrite ?? false);
}
// Referenced from https://github.com/reactjs/react.dev/blob/6570e6cd79a16ac3b1a2902632eddab7e6abb9ad/src/content/reference/react/Suspense.md
/**
* A custom hook like [`React.use`](https://react.dev/reference/react/use) hook using private Suspense implementation.
*/
function use(promise) {
if (isReactUsePromise(promise)) {
if (promise.status === 'fulfilled') {
if (promise.value === undefined) {
throw new Error('[use] Unexpected undefined value from promise');
}
return promise.value;
}
else if (promise.status === 'rejected') {
throw promise.reason;
}
else if (promise.status === 'pending') {
throw promise;
}
throw new Error('[use] Promise is in an invalid state');
}
const suspensePromise = promise;
suspensePromise.status = 'pending';
suspensePromise.then((result) => {
suspensePromise.status = 'fulfilled';
suspensePromise.value = result;
}, (reason) => {
suspensePromise.status = 'rejected';
suspensePromise.reason = reason;
});
throw suspensePromise;
}
function isReactUsePromise(promise) {
return typeof promise === 'object' && promise !== null && 'status' in promise;
}
//#endregion
//# sourceMappingURL=hooks.js.map