React Hooks
@ahoo-wang/wow-react keeps a Wow query in a component: the query, its result, whether it is loading, and the error it failed with. A hook runs the query on mount and again whenever the query changes, cancels a request a newer query replaces, and cancels it on unmount. The queries are the ones of Read and Query, run through a generated query client.
Install
pnpm add react react-dom @ahoo-wang/fetcher @ahoo-wang/fetcher-eventstream \
@ahoo-wang/wow-client @ahoo-wang/wow-reactReact 19 or later: the package is built with the React Compiler and imports react/compiler-runtime, which React 18 does not have.
Run a query through a client
A use*Query hook takes the query and an execute function that runs it; a generated client's method takes exactly the query and the request options, so the client supplies the route and the response handling:
import type { SnapshotRecord } from '@ahoo-wang/wow-client';
import { desc, filter, pagedQuery } from '@ahoo-wang/wow-client/dsl';
import { usePagedQuery } from '@ahoo-wang/wow-react';
import type {
OrderSnapshotQueryClient,
OrderState,
OrderStateField,
} from './generated/ordering/index.js';
export function PaidOrders({
orders,
page,
}: {
orders: OrderSnapshotQueryClient;
page: number;
}) {
const { result, loading, error } = usePagedQuery<
SnapshotRecord<OrderState>,
OrderStateField | 'lastEventTime'
>({
query: pagedQuery({
filter: filter.eq('state.status', 'PAID'),
sort: [desc('lastEventTime')],
pagination: { index: page, size: 20 },
}),
execute: (query, options) => orders.paged(query, options),
});
if (error) return <p role="alert">{error.message}</p>;
if (loading || !result) return <p>Loading…</p>;
return (
<ul>
{result.list.map(order => (
<li key={order.aggregateId}>
{order.aggregateId}: {order.state.totalAmount}
</li>
))}
</ul>
);
}- Pass the record type and the fields the query names as the type arguments: TypeScript does not infer the fields from
executeonce the first argument is written. - Hand the options on to the client, as above: they carry the
signalthe hook cancels a request with. - Pass the method through an arrow function: a method passed alone loses its
this.
| Hook | Runs | result |
|---|---|---|
useSingleQuery | singleQuery(…) | One record |
useListQuery | listQuery(…) | The records |
usePagedQuery | pagedQuery(…) | { total, exact, list } |
useCountQuery | { filter, limit? } | { count, exact } |
useListStreamQuery | listQuery(…), as server-sent events | items as they arrive, then done |
Aggregations and cursor queries have no hook of their own. Run a cursor query through useSingleQuery with the page as its result, and an aggregation with client.aggregate(query, { signal }) in an effect that aborts on cleanup.
Stream a long list
useListStreamQuery reads a list as server-sent events and keeps the rows as they arrive, rendering at most about once a frame however many come. A stream the service cuts off is resumed where it stopped, as listStream resumes it:
import type { SnapshotRecord } from '@ahoo-wang/wow-client';
import { filter, listQuery } from '@ahoo-wang/wow-client/dsl';
import { useListStreamQuery } from '@ahoo-wang/wow-react';
import type {
OrderSnapshotQueryClient,
OrderState,
OrderStateField,
} from './generated/ordering/index.js';
export function ShippedOrders({ orders }: { orders: OrderSnapshotQueryClient }) {
const { items, done, loading, error, abort } = useListStreamQuery<
SnapshotRecord<OrderState>,
OrderStateField
>({
query: listQuery({ filter: filter.eq('state.status', 'SHIPPED'), limit: 1000 }),
execute: (query, options) => orders.listStream(query, options),
});
if (error) return <p role="alert">{error.message}</p>;
return (
<>
<p>
{items.length} orders{done ? '' : '…'}
</p>
{loading && <button onClick={abort}>Stop</button>}
</>
);
}abort() stops the stream and keeps the rows it read; reset() stops it and empties them.
State and errors
Every hook returns status ('idle' | 'loading' | 'success' | 'error'), loading, result (or items and done), error, and execute, abort, reset, getQuery and setQuery.
- A hook runs on mount and whenever its query changes, compared by content;
autoExecute: falseruns it only onexecute(). A query set toundefinedaborts the request in flight and leaves the hook idle. - A failed request or
abort()keeps the lastresult, so a failed refresh does not blank the screen. - In server rendering a hook renders as
loadingand sends nothing; the query runs once the page hydrates. erroris aWowProblemErrorfor a request the service refused, with itscode,statusandviolations; pass the error type as the third type argument to type it so.
Without a generated client
The useFetcher*Query hooks post a query to a query route through a Fetcher instead of a client, for a route no client covers, such as a dataset's: useFetcherPagedQuery({ fetcher, url: '/wow/dataset/eshop-ordering/sales/query', initialQuery }). The wow-react README lists their options.
The Storybook runs every hook against in-memory data.