React Hooks for a GraphQL Database API
@bifrostql/react is an experimental React client for BifrostQL APIs. It is not the client used by the shipped editor — the desktop app’s editor is built on @standardbeagle/edit-db, which has its own data layer. The companion @bifrostql/app-shell package is experimental too, and currently has no importers. Use @bifrostql/react for standalone experiments; expect its API to change. It’s built on TanStack Query, so you get caching, background refetching, and optimistic updates for free — you just describe the table, fields, filter, and sort.
Install
Section titled “Install”npm install @bifrostql/react @tanstack/react-query react react-domWrap your app once with the TanStack QueryClientProvider and BifrostProvider:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';import { BifrostProvider } from '@bifrostql/react';
const queryClient = new QueryClient();
export default function App() { return ( <QueryClientProvider client={queryClient}> <BifrostProvider config={{ endpoint: 'http://localhost:5000/graphql' }}> <UserList /> </BifrostProvider> </QueryClientProvider> );}Querying
Section titled “Querying”useBifrostQuery is the table-oriented hook — pass the table name and what you want back:
import { useBifrostQuery } from '@bifrostql/react';
function UserList() { const { data, isLoading, error } = useBifrostQuery('users', { fields: ['id', 'name', 'email'], filter: { active: true, age: { _gte: 18 }, role: { _in: ['admin', 'editor'] } }, sort: [{ field: 'name', direction: 'asc' }], pagination: { limit: 25 }, });
if (isLoading) return <div>Loading…</div>; if (error) return <div>{error.message}</div>; return <ul>{data?.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;}Filter operators mirror the API: _eq, _neq, _gt, _gte, _lt, _lte, _in, _nin, _contains, _ncontains, _starts_with, _ends_with, _null, _nnull.
The hook toolbox
Section titled “The hook toolbox”| Hook | Use it for |
|---|---|
useBifrost |
Low-level raw GraphQL — everything else builds on it |
useBifrostQuery |
Table reads with filter / sort / pagination |
useBifrostMutation |
Insert / update / upsert / delete with auto-invalidation |
useBifrostInfinite |
Infinite scroll (offset or cursor) |
useBifrostSubscription |
Real-time via graphql-transport-ws WebSocket or SSE (transport: 'auto') |
useBifrostDiff |
Send only changed fields, with conflict detection |
useBifrostBatch |
Sequential multi-mutation writes with progress, dependency-ordered |
useBifrostTable |
Headless table state: sort, filter, paginate, select, columns, URL sync, aggregates |
Mutations
Section titled “Mutations”Mutation builders generate the right GraphQL for each operation and invalidation keeps the cache fresh:
import { useBifrostMutation, buildInsertMutation } from '@bifrostql/react';
const insert = useBifrostMutation(buildInsertMutation('users'), { invalidateQueries: ['users'],});insert.mutate({ detail: { name: 'Alice', email: 'alice@example.com' } });buildInsertMutation, buildUpdateMutation, buildUpsertMutation, and buildDeleteMutation cover the four mutation shapes.
Efficient writes
Section titled “Efficient writes”useBifrostDiffpreviews and submits only the fields that actually changed, and flags conflicts if the server row drifted since you loaded it.useBifrostBatchruns many operations in one go, sorted into insert → update → delete order, withonProgressreporting and optionalallowPartialSuccess.
Headless tables
Section titled “Headless tables”useBifrostTable is a complete table engine without any markup — sorting, multi-column filtering, pagination, row selection, expandable rows, column show/hide and reorder, optional urlSync, client computed columns, and built-in count/sum/avg/min/max aggregates. Bring your own UI, or drop in the pre-built BifrostTable component (theming, inline editing, CSV export, row actions).
Editing follows the server’s policy
Section titled “Editing follows the server’s policy”Both useBifrostTable and BifrostTable read the table’s _dbSchema projection for the caller (the same document usePolicy reads, fetched once per identity and shared through the query cache) and shape themselves from it:
editabledefaults to'auto': the table takes edits only when the projection’sallowedActionsincludesupdateand a write handler is wired (onRowUpdateon the component;onRowUpdateoronBatchSaveon the hook). Passtrueto assert editing — the component still throws withoutonRowUpdate— orfalseto switch it off;falsewins over any projection.- A column takes an editor only when the projection marks it
writable. A column config’seditable: trueopts a column in, but cannot grant what the server withholds. - A column projected
readable: falserenders its cell as—, so a withheld value never reads as a null. - A row carrying
_can { update delete }follows its own answer; a row without it inherits the table’s. Row actions declare the capability they need withpermission: 'update' | 'delete'and render only where it holds; an action withoutpermissionalways renders. - If the policy document fails to load, the table renders read-only and
BifrostTableshows the error above the rows. The hook exposes the projection aspolicy, plusrowCan(row, action)andisColumnMasked(field)for a custom UI.
<BifrostTable table="members" columns={columns} identity={session.userId} onRowUpdate={(row, changes) => update.mutateAsync({ id: row.id, ...changes })} rowActions={[ { label: 'Delete', permission: 'delete', onClick: remove }, { label: 'View', onClick: open }, ]}/>Want a whole admin UI instead of hooks?
Section titled “Want a whole admin UI instead of hooks?”If you don’t want to assemble screens by hand, the Embeddable Data Editor gives you a complete, schema-driven CRUD navigator as a single <Editor> component.