@mantlejs/react
React hooks for Mantle JS services, built on TanStack Query v5 over the @mantlejs/client SDK.
Installation
Section titled “Installation”npm install @mantlejs/react @mantlejs/client @tanstack/react-query reactreact, @tanstack/react-query, and @mantlejs/client are peer dependencies — the package ships no React runtime or query cache of its own.
Concepts
Section titled “Concepts”Bindings, not a client
Section titled “Bindings, not a client”@mantlejs/react does not talk to a Mantle server itself. It wraps a MantleClient instance (from @mantlejs/client) and exposes it to a React component tree: query hooks for find/get, mutation hooks for create/update/patch/remove, all dispatched through the client’s REST transport (auth headers, 401 refresh-retry, and typed errors included).
Provider at the root, hooks in the tree
Section titled “Provider at the root, hooks in the tree”MantleProvider owns the client instance and a TanStack QueryClient (it creates a default one when none is passed). Hooks anywhere below it resolve both from context — no prop drilling, no module-level singletons.
Deterministic query keys
Section titled “Deterministic query keys”Query keys follow [service, method, ...identifiers]:
| Hook call | Query key |
|---|---|
useFind("messages") |
["messages", "find"] |
useFind("messages", params) |
["messages", "find", params] |
useGet("messages", "42") |
["messages", "get", "42"] |
useGet("messages", "42", params) |
["messages", "get", "42", params] |
Invalidation always targets the [service] prefix, so find and get caches for a service invalidate together.
Real-time cache invalidation
Section titled “Real-time cache invalidation”When the client was created with the socket option, useFind and useGet subscribe to the service’s created / updated / patched / removed events and call queryClient.invalidateQueries({ queryKey: [service] }) when one arrives. Subscriptions are reference-counted per service — one listener set no matter how many hooks are mounted, removed when the last hook unmounts. Opt out per hook with realtime: false.
Event delivery is at-most-once (see the @mantlejs/sync README): events emitted while the socket is disconnected are gone. To bound staleness, MantleProvider listens for the client’s 'reconnect' event and invalidates every query on re-connect, forcing mounted queries to refetch.
Mutations do not invalidate
Section titled “Mutations do not invalidate”useCreate / useUpdate / usePatch / useRemove rely on the server’s socket event to trigger invalidation. Without a socket, or for optimistic updates, pass TanStack’s onSuccess / onMutate callbacks.
Quick start
Section titled “Quick start”import { mantle } from "@mantlejs/client";import { MantleProvider, useCreate, useFind } from "@mantlejs/react";
const client = mantle({ url: "http://localhost:3030", socket: {} });
export function App() { return ( <MantleProvider client={client}> <Messages /> </MantleProvider> );}
interface Message { id: number; text: string;}
function Messages() { const { data, isPending } = useFind<Message>("messages", { query: { $limit: 25, $sort: { id: "desc" } } }); const create = useCreate<Message>("messages");
if (isPending) return <p>Loading…</p>; const messages = Array.isArray(data) ? data : (data?.data ?? []); return ( <> <ul> {messages.map((m) => ( <li key={m.id}>{m.text}</li> ))} </ul> <button onClick={() => create.mutate({ text: "Hello" })}>Send</button> </> );}When another client creates a message, the server broadcasts messages created over Socket.IO, the hook invalidates ["messages"], and the list refetches — no wiring needed.
MantleProvider
Section titled “MantleProvider”Wraps children in a QueryClientProvider and stores the MantleClient in context.
| Prop | Type | Default | Description |
|---|---|---|---|
client |
MantleClient |
required | The @mantlejs/client instance hooks dispatch through |
queryClient |
QueryClient |
new QueryClient() |
Custom TanStack Query client |
useMantleClient(): MantleClient
Section titled “useMantleClient(): MantleClient”The client from the nearest MantleProvider. Throws a descriptive Error when used outside one. Use it for anything the hooks don’t cover — client.authenticate(...), client.service("docs").similar(...), auth events.
Query hooks
Section titled “Query hooks”useFind<T>(service, params?, options?); // GET /:service → UseQueryResult<T[] | Paginated<T>, MantleClientError>useGet<T>(service, id, params?, options?); // GET /:service/:id → UseQueryResult<T, MantleClientError>options accepts every TanStack useQuery option except queryKey/queryFn, plus:
| Option | Type | Default | Description |
|---|---|---|---|
realtime |
boolean (useGet), boolean | RealtimePatchOptions<T> (useFind) |
true when a socket is configured |
Socket-driven cache invalidation for this hook, or in-place patching (below) |
In-place realtime (useFind only)
Section titled “In-place realtime (useFind only)”The default realtime behavior invalidates the service’s queries, so every event triggers a refetch. With
realtime: { mode: "patch" }, useFind writes created / updated / patched / removed events straight into its
own cached result instead. The list changes in place with no request, and a Paginated<T> envelope’s total stays in
step.
const { data } = useFind<Comment>( "comments", { query: { articleId, $sort: { createdAt: 1 } } }, { realtime: { mode: "patch", matches: (comment) => comment.articleId === articleId, // mirror the query's filter insert: "end", // mirror its $sort: where created records go idField: "id", // default }, },);Events carry the full record, but the client can’t re-run the server’s where/$sort, which is what matches and
insert are for. A changed record that stops matching is removed, and one that starts matching is inserted. Only this
hook’s cache entry is touched. Events missed while the socket was disconnected are recovered by the provider’s
invalidate-everything on reconnect.
Mutation hooks
Section titled “Mutation hooks”useCreate<T>(service, options?); // variables: Partial<T> → POST /:serviceuseUpdate<T>(service, options?); // variables: { id: Id; data: Partial<T> } → PUT /:service/:idusePatch<T>(service, options?); // variables: { id: Id; data: Partial<T> } → PATCH /:service/:iduseRemove<T>(service, options?); // variables: Id → DELETE /:service/:idoptions accepts every TanStack useMutation option except mutationFn. Errors are typed MantleClientError.
| Type | Description |
|---|---|
MantleProviderProps |
Props for MantleProvider (client, queryClient?, children) |
MantleQueryOptions |
The realtime?: boolean extension accepted by useGet |
MantleFindOptions<T> |
useFind’s extension: realtime?: boolean | RealtimePatchOptions<T> |
RealtimePatchOptions<T> |
{ mode: "patch"; idField?; matches?; insert? } — in-place realtime for useFind |
Development
Section titled “Development”npx nx build react # compilenpx nx test react # run testsnpx nx lint react # lintPublishing
Section titled “Publishing”Build before publishing:
npx nx build reactFirst publish (scoped packages require --access public):
cd packages/reactnpm publish --access publicSubsequent releases — bump version in packages/react/package.json, then:
cd packages/reactnpm publishTesting locally with Verdaccio
Section titled “Testing locally with Verdaccio”# Terminal 1 — start the local registrynpx nx run @mantle/source:local-registry
# Terminal 2 — publish to itcd packages/reactnpm publish --registry http://localhost:4873
# Install from it in another projectnpm install @mantlejs/react --registry http://localhost:4873