README.md

@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.