Hooks
Three hooks cover the foreground reactive surface: useQuery for a single live
result set, useInfiniteQuery for paginated reads, and useDatabaseReady for
gating rendering on SalveDbProvider's boot state. All three are cache-backed
and re-run automatically on any write to the table involved — regardless of
whether the write came from this hook, another screen, raw SQL, a migration,
or the native sync engine.
useQuery
function useQuery<TSchema extends AnySchema>(props: {
schema: TSchema;
queryFn: (q: SelectQueryBuilder<TSchema>) => SelectQueryBuilder<TSchema>;
deps?: readonly JsonValue[];
}): {
data: InferSelectModel<TSchema>[] | null;
isLoading: boolean;
error: unknown;
};
schema— the schema to read from and to subscribe to. AnyINSERT/UPDATE/DELETEagainst this table re-runsqueryFnand re-renders.queryFn— receives aselectquery builder already scoped toschema; applywhere/orderBy/limit/offsethere. See the Query Builder guide.deps— extra reactive inputs (JSON-serializable) beyondschema, e.g. a search term. Changingdepsre-runs the query the same way a table write does.
data is null until the first result lands; isLoading is true while
SalveDbProvider is still booting (useDatabaseReady) or while this specific
query hasn't produced a result yet; error surfaces either a provider boot
error or a query execution error.
If schema.sync.enabled is true, mounting the hook also triggers a
throttled, read-triggered sync: reading a synced table nudges
Database.sync(schema.name) in the background instead of requiring you to
call it yourself. See Sync for the underlying push/pull contract.
import { useQuery } from '@salve-software/react-native-salve-db';
import { eq } from '@salve-software/react-native-salve-db';
import { UserSchema } from './schemas/UserSchema';
function UserList({ search }: { search: string }) {
const { data, isLoading, error } = useQuery({
schema: UserSchema,
queryFn: (q) => q.where(eq('name', search)).orderBy('updatedAt', 'desc').limit(50),
deps: [search],
});
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorBanner error={error} />;
return (
<FlatList
data={data ?? []}
keyExtractor={(user) => String(user.id)}
renderItem={({ item }) => <UserRow user={item} />}
/>
);
}
Because data reflects every write to users from any source, editing a row
from Studio, applying a sync pull, or writing from another
screen all re-render this list without any manual invalidation.
useInfiniteQuery
function useInfiniteQuery<TSchema extends AnySchema>(props: {
schema: TSchema;
queryFn: (q: SelectQueryBuilder<TSchema>) => SelectQueryBuilder<TSchema>;
pageSize: number;
deps?: readonly JsonValue[];
}): {
data: Row<TSchema>[] | null;
isLoading: boolean;
error: unknown;
hasNextPage: boolean;
fetchNextPage: () => void;
};
Same live-table semantics as useQuery, plus pagination:
queryFnsetswhere/orderByonly — do not call.limit()/.offset()yourself, the hook manages paging and forwardspageSizeinternally.fetchNextPage()loads the next page and appends it todata(a no-op oncehasNextPageisfalse).datais every loaded page flattened into a single array, in fetch order.
Any write to schema's table resets pagination back to page 0 and
refetches from the start — this hook is not a scroll-preserving "insert at
the top" feed. A background sync pull landing mid-scroll, or another screen
inserting a row, collapses accumulated pages and starts over, trading scroll
position for guaranteed on-disk consistency.
import { useInfiniteQuery } from '@salve-software/react-native-salve-db';
import { TaskSchema } from './schemas/TaskSchema';
function TaskFeed() {
const { data, isLoading, hasNextPage, fetchNextPage } = useInfiniteQuery({
schema: TaskSchema,
queryFn: (q) => q.orderBy('createdAt', 'desc'),
pageSize: 20,
});
return (
<FlatList
data={data ?? []}
keyExtractor={(task) => String(task.id)}
renderItem={({ item }) => <TaskRow task={item} />}
onEndReached={() => hasNextPage && fetchNextPage()}
refreshing={isLoading}
/>
);
}
useDatabaseReady
function useDatabaseReady(): {
isReady: boolean;
isLoading: boolean;
error: unknown;
};
Reads the readiness state set by the nearest SalveDbProvider ancestor.
SalveDbProvider runs Database.configure(config) then Database.register(schema)
for every schema in schemas on mount, and sets:
{ isReady: false, isLoading: true, error: null }while configure/register are running,{ isReady: true, isLoading: false, error: null }once every schema has registered successfully,{ isReady: false, isLoading: false, error }if configure or any register call throws.
useQuery and useInfiniteQuery already call this internally and fold its
state into their own isLoading/error — you only need useDatabaseReady
directly when gating something outside a query, such as the app shell itself.
import { useDatabaseReady } from '@salve-software/react-native-salve-db';
function AppShell({ children }: { children: React.ReactNode }) {
const { isReady, isLoading, error } = useDatabaseReady();
if (error) return <BootErrorScreen error={error} />;
if (isLoading || !isReady) return <SplashScreen />;
return <>{children}</>;
}