React Hooks
@ahoo-wang/wow-react 在组件中保存一个 Wow 查询:查询本身、它的结果、是否正在加载,以及它失败时的错误。hook 在挂载时运行查询,查询改变时再次运行,取消被更新的查询替换掉的请求,并在卸载时取消请求。这些查询就是读取与查询中的那些,通过生成的查询客户端运行。
安装
pnpm add react react-dom @ahoo-wang/fetcher @ahoo-wang/fetcher-eventstream \
@ahoo-wang/wow-client @ahoo-wang/wow-react需要 React 19 或更高版本:这个包用 React Compiler 构建,会导入 react/compiler-runtime,而 React 18 没有它。
通过客户端运行查询
use*Query hook 接收查询和一个运行它的 execute 函数;生成客户端的方法恰好接收查询和请求选项,所以由客户端提供路由和响应处理:
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>
);
}- 把记录类型和查询指明的字段作为类型参数传入:第一个参数写出来之后,TypeScript 不会再从
execute推断字段。 - 像上面那样把选项交给客户端:它们带着 hook 用来取消请求的
signal。 - 通过箭头函数传递方法:单独传递的方法会丢失它的
this。
| Hook | 运行 | result |
|---|---|---|
useSingleQuery | singleQuery(…) | 一条记录 |
useListQuery | listQuery(…) | 记录列表 |
usePagedQuery | pagedQuery(…) | { total, exact, list } |
useCountQuery | { filter, limit? } | { count, exact } |
useListStreamQuery | listQuery(…),以服务端推送事件 | 陆续到达的 items,然后是 done |
聚合查询和游标查询没有自己的 hook。游标查询通过 useSingleQuery 运行,以这一页作为它的结果;聚合则在一个清理时会中止的 effect 中用 client.aggregate(query, { signal }) 运行。
流式读取长列表
useListStreamQuery 以服务端推送事件读取列表,并保存陆续到达的行,无论到达多少行,大约每帧至多渲染一次。服务切断的流会从停下的地方续读,就像 listStream 那样:
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() 停止流,保留已读到的行;reset() 停止流并清空它们。
状态与错误
每个 hook 都返回 status('idle' | 'loading' | 'success' | 'error')、loading、result(或 items 和 done)、error,以及 execute、abort、reset、getQuery 和 setQuery。
- hook 在挂载时以及查询改变时(按内容比较)运行;
autoExecute: false让它只在execute()时运行。设为undefined的查询会中止在途的请求,让 hook 处于空闲状态。 - 失败的请求或
abort()会保留上一次的result,所以失败的刷新不会让屏幕变空。 - 在服务端渲染中,hook 渲染为
loading且不发送任何请求;页面水合之后才运行查询。 - 对服务拒绝的请求,
error是一个WowProblemError,带着它的code、status和violations;把错误类型作为第三个类型参数传入,就能这样为它提供类型。
不用生成的客户端
useFetcher*Query hooks 通过 Fetcher 而不是客户端把查询发往查询路由,用于没有客户端覆盖的路由,例如数据集的路由:useFetcherPagedQuery({ fetcher, url: '/wow/dataset/eshop-ordering/sales/query', initialQuery })。它们的选项见 wow-react README。
Storybook 对着内存数据运行每个 hook。