TanStack Query integration
How the SDK rides TanStack Query — the provider-owned QueryClient, the key namespace, automatic invalidation, manual invalidation, and what clears on sign-out and re-bootstrap.
Every @capxul/sdk-react hook is a thin binding over TanStack Query: query
hooks return UseQueryResult, mutation hooks return UseMutationResult, and
all reactivity — caching, refetching, invalidation — is TanStack's. This guide
explains the pieces you interact with when your app shares the cache with the
SDK.
Before you start
@tanstack/react-queryis installed (it is a peer dependency — the quickstart installs it).CapxulProvideris mounted; you do not mount aQueryClientProvideryourself unless you bring your own client (below).
The provider owns the QueryClient
CapxulProvider creates a QueryClient and renders its own
QueryClientProvider, so the hooks work with zero TanStack setup. The
defaults it creates:
new QueryClient({
defaultOptions: {
queries: { retry: 2, staleTime: 30_000 },
mutations: { retry: 0 },
},
});- Queries retry twice and are considered fresh for 30 seconds.
- Mutations never retry — money movements must not be silently re-fired.
Because the provider renders a real QueryClientProvider, your own
useQuery/useMutation calls inside the tree resolve the same client and
share its cache.
Bring your own QueryClient
Pass queryClient to the provider to share a client you configure yourself
(your own defaults, devtools, persistence):
"use client";
import { QueryClient } from "@tanstack/react-query";
import { CapxulProvider } from "@capxul/sdk-react";
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: 1 } },
});
export function Providers({ children }: { children: React.ReactNode }) {
return (
<CapxulProvider publishableKey={publishableKey} queryClient={queryClient}>
{children}
</CapxulProvider>
);
}The client is pinned at mount: swapping the queryClient prop later is
ignored. Create it once (module scope or useState initializer) and leave it.
Ownership also changes cleanup behavior — see what clears when.
The key namespace
Every SDK query key is an array whose first element is the literal
"capxul". The catalog is hierarchical; representative shapes:
| Data | Key |
|---|---|
| Profile | ["capxul", "profile"] |
| Personal balance | ["capxul", "accountBalance"] |
| Org list | ["capxul", "orgs"] |
| One org | ["capxul", "org", orgId] |
| Org members / roles / treasury | ["capxul", "org", orgId, "members" | "roles" | "treasury"] |
| Payments list | ["capxul", "payments"] |
| One payment | ["capxul", "payments", paymentId] |
| Actor-scoped reads (address book and inbox) | ["capxul", "actor", "account" | "org", …] |
| Destinations | ["capxul", "destinations", …] |
Parameterized keys use the placeholder "pending" while their parameter is
still undefined (the query is disabled in that state, so nothing fetches
under a placeholder key).
The catalog object itself is internal — the stable contract is the "capxul"
prefix plus the exact key documented on each hook's reference page (under
"Query key"). If you call the Core SDK directly, put
your custom keys under the same prefix.
Automatic invalidation
You should rarely invalidate SDK data yourself. Two families of mutations do it for you:
Money mutations — personal and Organization payment or Commitment hooks invalidate, on success:
- the payments list and the affected payment's detail key,
- the personal balance — or, for an org actor, that org's account and treasury keys,
- the actor's address book (for
pay, which can create a new counterparty).
Auth boundary changes:
- identity operations update the provider-owned actor directly; rich-data queries keep their own TanStack Query authorities.
- successful
useCapxulAuth().signOut()cancels and resets the authenticated rich-data query boundary.
Org mutations invalidate their own slices: useCapxulCreateOrg → the org
list; useCapxulInviteMember → that org's member list. Each hook's reference
page documents its exact cache behavior.
Manual invalidation
For anything the automatic rules do not cover (for example a server-driven
change you learn about out of band), use useQueryClient from
@tanstack/react-query. To target every Capxul query, predicate on the first
key element:
"use client";
import { useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
// Refetch everything the SDK has cached:
await queryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === "capxul",
});
// Or one slice, using the documented key shape:
await queryClient.invalidateQueries({ queryKey: ["capxul", "payments"] });invalidateQueries with a key prefix matches hierarchically —
["capxul", "payments"] invalidates the list and every
["capxul", "payments", paymentId] detail under it.
What clears on sign-out and re-bootstrap
Two different events, two different scopes:
Sign-out resets the authenticated Capxul query boundary after the identity machine signs out. If your app stores additional sensitive queries outside that boundary, remove them by predicate after the facade call succeeds:
const auth = useCapxulAuth();
const queryClient = useQueryClient();
await auth.signOut();
queryClient.removeQueries({
predicate: (query) => query.queryKey[0] === "capxul",
});Re-bootstrap — the provider replacing its client (a retry after a
bootstrap error, a publishableKey change, unmount) — clears client-scoped
data so nothing from the previous client leaks into the next one:
- Provider-owned QueryClient: the provider calls
queryClient.clear()— everything goes, including your app's own queries on that client. - Your own QueryClient: the provider removes only queries whose
queryKey[0] === "capxul"— your app's queries survive.
That asymmetry is the main reason to bring your own QueryClient once your app caches non-Capxul data in the same tree.