useQuery
useQuery reads a query from the @toapi/client and returns its resolved value, keeping that value up to date as the underlying cache entry is revalidated. It is the primary hook in @toapi/react.
import { useQuery } from "@toapi/react";Signature
Section titled “Signature”function useQuery<T>( query: ObservablePromise<T> | (() => ObservablePromise<T>), options?: { startTransition?: typeof React.startTransition; },): T;
type ObservablePromise<T> = Promise<T> & Observable<T>;Parameters
Section titled “Parameters”query— either anObservablePromise<T>or a factory returning one. This is exactly what a client.get()produces: aPromise<T>that also implementsObservable<T>(it has a.subscribe()method). Pass the promise directly for a static query, or a factory function when the query depends on props/state — the factory is memoized on its identity, so build it withuseCallbackor inline it carefully to control when a new request is issued.options.startTransition— thestartTransitionfunction used to apply background updates. Defaults to React’sReact.startTransition. Override it to route updates through a custom transition (for example one wired touseTransition’s pending state).
Return value
Section titled “Return value”useQuery returns the resolved value T directly — not a promise, and not a { data, isLoading } wrapper.
- On first render it calls React’s
use()on the query, so the component suspends until the value resolves. Wrap the component in aSuspenseboundary to show a fallback, and in an error boundary to catch rejections (such as anHttpError). - On subsequent updates it returns the latest value delivered by the subscription, applied inside
startTransitionso the UI stays responsive during the refresh.
What it does
Section titled “What it does”Internally useQuery does two things with the object you pass:
- Suspends via
use(). Until the first value arrives, the hook callsReact.use(query), which suspends the component until the promise resolves (or throws to the nearest error boundary). - Subscribes to the client’s PubSub. It calls
query.subscribe(...)and re-renders whenever the client emits a new value for that entry. The client emits when the query is invalidated through Toapi’s tag-based cache invalidation, when the server pushes an out-of-band invalidation, or when you manually call arevalidatemethod such asclient.books.revalidate(). The subscription is torn down automatically on unmount.
This means a component reading client.books.get() will re-render with fresh data after any mutation that affects the books tag — without you wiring up any additional state.
Basic read with Suspense
Section titled “Basic read with Suspense”import { Suspense } from "react";import { useQuery } from "@toapi/react";import { client } from "./tapi-client";
function BookList() { const books = useQuery(client.books.get());
return ( <ul> {books.map((book) => ( <li key={book.id}>{book.name}</li> ))} </ul> );}
export function Page() { return ( <Suspense fallback={<p>Loading books…</p>}> <BookList /> </Suspense> );}A query that depends on state
Section titled “A query that depends on state”When the query depends on props or state, pass a factory so a new request is built when the inputs change. Memoize it so the hook does not resubscribe on every render.
import { useCallback, useState } from "react";import { useQuery } from "@toapi/react";import { client } from "./tapi-client";
function SearchResults({ initial = "" }: { initial?: string }) { const [q, setQ] = useState(initial);
const results = useQuery( useCallback(() => client.books.get({ q }), [q]), );
return ( <> <input value={q} onChange={(e) => setQ(e.target.value)} /> <ul> {results.map((book) => ( <li key={book.id}>{book.name}</li> ))} </ul> </> );}Automatic updates after a mutation
Section titled “Automatic updates after a mutation”Because useQuery subscribes to the client, a component reading a query updates itself once a mutation invalidates the matching tags — no manual refetch needed.
import { useQuery } from "@toapi/react";import { client } from "./tapi-client";
function Books() { const books = useQuery(client.books.get());
async function addBook() { // The POST response carries tags that overlap the GET above, // so the list re-renders with the new book automatically. await client.books.post({ name: "New Book" }); }
return ( <> <button onClick={addBook}>Add book</button> <ul> {books.map((book) => ( <li key={book.id}>{book.name}</li> ))} </ul> </> );}See also
Section titled “See also”@toapi/react— package overview and installation.@toapi/client— the client that produces the queries you pass here.Observable— the subscription primitiveuseQuerybuilds on.- Revalidation & subscriptions — how tag-based invalidation drives updates.