@folio/react-query #
A small TanStack Query v5 adapter for @folio/client.
bun add @folio/client @folio/react-query @tanstack/react-query react react-dom
These are the intended package names; they have not been published by this
repository. cargo make pack creates both tarballs in target/packages.
For a local app before publication, list both tarballs as dependencies and pin the unpublished peer so Bun does not try resolving it through the registry:
{
"dependencies": {
"@folio/client": "file:/absolute/path/folio-client-0.1.0.tgz",
"@folio/react-query": "file:/absolute/path/folio-react-query-0.1.0.tgz",
"@tanstack/react-query": "^5.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"overrides": {
"@folio/client": "file:/absolute/path/folio-client-0.1.0.tgz"
}
}
Open one Folio and one QueryClient for the application, and connect them before mounting queries. React context can be shared within one React tree; an Astro app should mount gallery, upload, and delete in the same island.
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider, useMutation, useQuery } from "@tanstack/react-query";
import { openFolio, type Key } from "@folio/client";
import { connectFolio, folioListOptions } from "@folio/react-query";
const folio = await openFolio({ did: accountDid, fetch: authenticatedFetch });
const queryClient = new QueryClient();
const disconnect = connectFolio(queryClient, folio);
type Image = { $type: "pics.pixl.image"; alt: string };
function Gallery() {
const images = useQuery(folioListOptions<Image>(folio, { collection: "pics.pixl.image" }));
const save = useMutation({
networkMode: "always",
retry: false,
mutationFn: ({ key, value }: { key: Key; value: Image }) => folio.put(key, value),
});
if (images.isPending) return <p>Loading images…</p>;
if (images.isError) return <p>{images.error.message}</p>;
return <ul>{images.data.map(({ key, value }) =>
<li key={key.rkey}>
<input aria-label="Alt text" defaultValue={value.alt}
onBlur={(event) => save.mutate({ key, value: { ...value, alt: event.target.value } })} />
</li>,
)}</ul>;
}
const root = createRoot(document.getElementById("root")!);
root.render(<QueryClientProvider client={queryClient}><Gallery /></QueryClientProvider>);
// When this application's owner disposes the tree:
async function dispose() {
root.unmount();
disconnect();
queryClient.clear();
await folio.close();
}
folioRecordOptions(folio, { collection, rkey }) provides the same behavior for a
point read, returning null for an absent record. Generic record types provide
compile-time types; they do not perform Lexicon validation. Use your generated
models and validation at the application boundary.
Both helpers use networkMode: "always", staleTime: Infinity, and retry: false.
Folio owns persistence and optimistic edits; Query owns the UI cache. Mutation
success means saved locally. Call await folio.sync() separately to publish;
handle its error in the app. Avoid a second optimistic rollback or offline
mutation queue in Query. The first collection read still needs the PDS; a cached
collection can be queried and edited offline.
connectFolio subscribes once per Folio/QueryClient pair, invalidates this
account/database's queries after commits, and repairs missed tab hints on focus,
pageshow, or visibility resume. Repeated connections are reference counted and
each cleanup is idempotent, so React effect setup/cleanup can reconnect safely.
Connect before queries start. During an overlapping commit, query helpers reread
before returning. Initial and resume invalidations read cached data; they do not
schedule PDS synchronization.
Queries are scoped by database, account, and collection. On account change, disconnect the previous Folio and close it after its consumers unmount. Create a new client for the new account. See the client package for Wasm asset loading, storage behavior, and current protocol limitations.