Kanzo UI
The design of Kanzo UI

One read per datum

Why every read the library makes for a host — a relation's fields, an answer's rows — is a TanStack Query query inside the library's own hook, on the host's QueryClient, with no queryOptions surface; and what would reverse it.

The same datum was read by several parts of one page, each in its own way. A relation's fields were read by the dashboard's useFieldStats, again by AnswerCard over the same relation, and a third time by readAnswerRelations for Ask's prompt, so a page drew one relation's summary two or three times and the model and the card could be told different distinct counts. Over a corpus none of those reads is left: what a field holds is the corpus's profile, on the join graph (What a field is). A host that read anything else of its own wrote a promise memo that forgot failures — keasy wrote three, and a guard to stop a fourth. Each was a cache, and none of them knew about the others.

The library reads through TanStack Query, on the host's client

Every read the library makes on a host's behalf is a TanStack Query query, made inside the library's own hook, on the QueryClient the host's QueryClientProvider puts in context. The hook is the surface — useFieldStats, Ask's useAskChat, useSession — and the library's surface has no queryOptions, no keys and no client of its own. @tanstack/react-query is a peer of the entries that read, and only of them: @kanzo-tech/ui/analytics and @kanzo-tech/ai/data, each an optional peer reached only from that subpath through the engine rule (decided 13 October 2026). @kanzo-tech/ai's root — Chat, Assist, the chat hooks — reads nothing through it, so a host that only draws a field the model helps fill, as metadata-form does, installs nothing new. @kanzo-tech/auth is the third reader: AuthProvider reads the session useSession hands out as a query, and since the provider is on its root, there the peer is required (the auth layer).

The reference is the React integrations of tRPC and of Supabase. @trpc/react-query's hooks run useQuery inside the procedure's own hook, on the client the host's QueryClientProvider holds; Supabase Cache Helpers' useQuery does the same over a PostgREST query, deriving the key from the query. What both settle is the split this page takes: the library owns what is read and how it is keyed; the host owns the client — its defaults, its devtools, its persistence, its garbage collection — and has exactly one of it.

What it bought, each a defect before:

  • One read, deduplicated. The dashboard and an answer's card ask for a relation's fields under one key, and the second asker gets the first's answer, never a second SUMMARIZE. The key is the relation's: what a part hides (exclude) is applied to the read afterwards, so two parts that hide different columns still read once.
  • An abort is the query's. A query function is handed a signal. An answer's read hands it to Engine.query, and Stop cancels the query, so the wait ends at once; a SUMMARIZE still queued on the coordinator when every part that asked has gone is taken off its queue.
  • A failure is kept out of the cache. A rejected read is an error state with the thrown value whole, and the next asker reads again; the memo keasy wrote three times is the cache's default.

Discarded: queryOptions from each package, read by the host's own useQuery. It was the review's first proposal, and it is TanStack's own shape for a library of query definitions. It makes two ways to read one datum — the hook a part uses inside, and the options a host wires itself — and the host then writes the hook, the error handling and the abort the library already wrote. Back if a host needs a read outside React or ahead of render — a server component prefetching a session, a loader — and then that one read gets its options beside its hook, and no other does.

Discarded: a private QueryClient inside the library. Two clients on one page, a cache the host cannot see in its devtools or clear on sign-out, and defaults it cannot set. Back if a host cannot render a provider at all — and every reading package is a React package.

What would reverse it: a consumer whose application caches with something else — SWR, Apollo — and refuses a second cache library. Then the reads become descriptions a small adapter per cache library runs, and TanStack's is the first adapter, not the only path.

Held by packages/ui/src/charts/field-stats.test.tsx, "reads a relation once for every part that asks, whatever each hides"; "reads on the host's client, and throws without one". packages/ai/src/data/ask-chat.test.ts, "ends the read with the chat's signal, keeps nothing of it, and reads again when asked again". The peer stays off both root entries by packages/ui/src/optional-peers.test.ts, "are reached from no package's root entry", which walks the import graph from each package's root module rather than asserting names — the peer arrived with no exported name at all, inside hooks.

A read is made once, and kept as long as the datum holds

A library query never retries, and is kept for as long as what it read cannot change. A retry is a second wait on one failure, which the failure rule forbids; so every query the library makes passes retry: false, whatever the host's client defaults to. A datum that cannot change passes staleTime and gcTime of Infinity: what it read is the page's for the page's life. A session changes under its key — renewed, ended, signed out in another tab — so it passes staleTime: 0, and is read again on the events the table names.

DatumRead byKeptRead again
a relation's SUMMARIZE, for one whose columns no corpus counted (What a field is)useFieldStats, and through it Dashboardthe page's lifewhen the relation or the provider's coordinator changes, which is another key
an answer's rowsuseAskChat, under the page's chipsthe page's life, by engine and statementwhen a chip or the answer changes, which changes the statement
a sessionAuthProvider, and through it useSession and Gateuntil it is stale, which is at oncewhen the tab becomes visible, after a renewal, and when another tab says so

The keys are the library's, and they are not an API. Each says what was read and from what — the coordinator or engine it was read on, and the relation or the compiled statement, which holds the page's clauses — so a new input is a new read, and nothing asks the host to invalidate by hand. A host that signs out clears its client, as it would for any other data of the person's.

Restoring a stored chat is not a query. A chat's messages are loaded once, before its first turn, by the chat hooks on @kanzo-tech/ai's root, which takes no TanStack peer; caching them would keep a second copy of a conversation the chat itself holds. When the chat hooks take load and save, load is a plain call.

What would reverse it: a datum the library caches that changes under the same key — a table a host rewrites in place under its name, a corpus rewritten under its attachment. Then that key gains the datum's own version — a corpus's from its manifest — and the rest stay as they are.

Held by packages/ui/src/charts/field-stats.test.tsx, "keeps a failure out of the cache, whole, and reads again for the next part that asks". packages/ai/src/data/ask-chat.test.ts, "reads an answer asked again under the same chips once, on the host's client".

On this page