Skip to content

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";
function useQuery<T>(
query: ObservablePromise<T> | (() => ObservablePromise<T>),
options?: {
startTransition?: typeof React.startTransition;
},
): T;
type ObservablePromise<T> = Promise<T> & Observable<T>;
  • query — either an ObservablePromise<T> or a factory returning one. This is exactly what a client .get() produces: a Promise<T> that also implements Observable<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 with useCallback or inline it carefully to control when a new request is issued.
  • options.startTransition — the startTransition function used to apply background updates. Defaults to React’s React.startTransition. Override it to route updates through a custom transition (for example one wired to useTransition’s pending state).

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 a Suspense boundary to show a fallback, and in an error boundary to catch rejections (such as an HttpError).
  • On subsequent updates it returns the latest value delivered by the subscription, applied inside startTransition so the UI stays responsive during the refresh.

Internally useQuery does two things with the object you pass:

  1. Suspends via use(). Until the first value arrives, the hook calls React.use(query), which suspends the component until the promise resolves (or throws to the nearest error boundary).
  2. 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 a revalidate method such as client.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.

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>
);
}

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>
</>
);
}

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>
</>
);
}