git:20260924.b36c6c8 to git:20261003.7cd1148

34 added, 18 removed. Audit A to A.

---
name: fetcher-react-hooks
description: >
- Drive React component state from requests with `@ahoo-wang/fetcher-react`: `useFetcher`, `useFetcherQuery`, `useQuery`, `useExecutePromise`, debounced variants, `useKeyStorage`, `useEventSubscription`, CoSec `SecurityProvider`/`RouteGuard`, and hooks generated from decorator services. Use for React components in a Fetcher app: loading/error state, abort on unmount, stale-result races, debounced search boxes. Not when a hook is no longer exported after the Fetcher 6 upgrade — load fetcher-v6-migration instead.
+ Drive React component state from requests with `@ahoo-wang/fetcher-react` 6: `useFetcher`, `useFetcherQuery`, `useQuery`, `useExecutePromise`, debounced variants, `useKeyStorage`, `useEventSubscription`, CoSec `SecurityProvider`/`RouteGuard`, and hooks generated from decorator services. Use for loading/error state, cancellation, stale-result races, debounced search boxes. Not for upgrading 5.x code or hooks missing after the 6 upgrade — load fetcher-v6-migration.
---
# fetcher-react-hooks
## Decisions
- - **Pick the lowest layer that fits**: `usePromiseState` (raw state) → `useExecutePromise` (execute/abort, unmount-safe) → `useFetcher` (one Fetcher request) / `useQuery` (your own query function) → `useFetcherQuery` (POST a query object). Debounced variants wrap each.
- - **Entry points**: the root entry has everything; `@ahoo-wang/fetcher-react/core` (state, query, debounce, utility hooks) and `@ahoo-wang/fetcher-react/fetcher` (`useFetcher`, `useFetcherQuery` and their debounced forms) are ESM-only subpaths without CoSec, storage or event-bus code.
+ - **Pick the lowest layer that fits**: `usePromiseState` (raw state) → `useExecutePromise` (execute/abort/reset, unmount-safe) → `useFetcher` (one Fetcher request) / `useQuery` (your own query function) → `useFetcherQuery` (POST a query object). Debounced variants wrap each.
+ - **Queries are controlled**: keep the query in your own React state and pass it as `query`; the hook executes whenever its content changes (deep-equal, so an inline object literal does not re-run). `query: undefined` means "not ready": nothing runs.
+ - **Entry points**: the root entry has everything; `@ahoo-wang/fetcher-react/core` (state, query, debounce hooks) and `@ahoo-wang/fetcher-react/fetcher` (`useFetcher`, `useFetcherQuery` and their debounced forms) are ESM-only subpaths without CoSec, storage or event-bus code.
- **From a decorator service**: `createQueryApiHooks({ api: service })` or `createExecuteApiHooks({ api: service })` generate `use<Method>` hooks instead of hand-written wrappers.
## Gotchas a capable model gets wrong
- - `useFetcher`'s `result` is the whole `FetchExchange` unless you pass `resultExtractor: ResultExtractors.Json`; `useFetcherQuery` defaults to JSON and sends `POST` with the query as the body.
- - `useQuery`, `useFetcherQuery` and `useQueryState` default `autoExecute` to `true` (run on mount); the debounced query hooks run automatically only with an explicit `autoExecute: true`.
- - `execute` takes a `PromiseSupplier` — `(abortController) => promise` — never a promise, and resolves to `void`: read data from `result` or `onSuccess`. Errors are kept in `error`, not thrown, unless `propagateError: true`.
- - Query hooks: change input with `setQuery` (it re-runs while `autoExecute` is on); `execute()` takes no arguments.
- - Debounced hooks expose `run`, `cancel` and `isPending()` (a function); `debounce: { delay }` is required.
- - `createExecuteApiHooks` hooks do not pass the AbortController to the service method, so `abort()` only drops the state update.
- - `useEventSubscription` re-subscribes when the `handler` object identity changes — memoize it.
+ - `execute` takes a `PromiseSupplier` — `(abortController) => promise` — never a promise. It **never rejects**: it resolves to `{ status, result, error }`, the state this execution ended in (`status: 'idle'` when it was cancelled or the component unmounted). Branch on `status`; do not wrap it in `try/catch`.
+ - Only the latest execution writes state: a new `execute` aborts the previous one, as do `abort()`, `reset()` and unmounting. `abort()` cancels an in-flight request (a settled result stays); `reset()` cancels and clears back to `idle`.
+ - `useFetcher`'s `result` is the whole `FetchExchange` unless you pass `resultExtractor: ResultExtractors.Json`; `useFetcherQuery` defaults to JSON and sends `POST url` with the query as the body. `exchange` is the exchange behind `result`, or behind `error` when it is an `ExchangeError` — read `exchange?.response?.status` of a 404 there.
+ - `useFetcher` owns cancellation: it sends `{ ...request, abortController }`, so an `abortController` you put on the request is replaced; cancel with `abort()`, or pass `request.signal`.
+ - Query hooks' `execute()` takes no arguments and re-runs the current query. `useQuery`'s `execute` option is `(query, abortController) => Promise<R>`. An auto-executing query renders `loading` on its first render.
+ - `useDebouncedQuery` / `useDebouncedFetcherQuery` follow the controlled `query`: the first query runs at once, later changes after `debounce.delay`. They return `pending` (a boolean) and `flush()` (apply the waiting query now). `useDebouncedValue(value, { delay })` debounces any value. `useDebouncedCallback`, `useDebouncedExecutePromise` and `useDebouncedFetcher` instead return `run(...args)`, `cancel()` and `isPending()`.
+ - Generated query hooks take `{ query, attributes, autoExecute }` and call `method(query, attributes, abortController)`. Generated execute hooks pass the controller to the method only with `appendAbortController: true`; otherwise `abort()` only drops the state update.
+ - `useEventSubscription` subscribes once per `bus` and handler `name`; it calls the latest `handle`, so an inline handler is fine.
## Minimal example
```tsx
- import { useFetcherQuery } from '@ahoo-wang/fetcher-react';
+ import { useState } from 'react';
+ import { useDebouncedFetcherQuery } from '@ahoo-wang/fetcher-react';
export function Search() {
- const { loading, result, error, setQuery } = useFetcherQuery<
+ const [query, setQuery] = useState({ keyword: '' });
+ const { loading, result, error, pending } = useDebouncedFetcherQuery<
{ keyword: string },
{ items: string[] }
- >({ url: '/api/search', initialQuery: { keyword: '' } });
- if (error) return <p>{String(error)}</p>;
+ >({ url: '/api/search', query, debounce: { delay: 300 } });
return (
<>
- <input onChange={e => setQuery({ keyword: e.target.value })} />
- {loading ? '…' : result?.items.join(', ')}
+ <input
+ value={query.keyword}
+ onChange={e => setQuery({ keyword: e.target.value })}
+ />
+ {error ? <p>{error.message}</p> : null}
+ {loading || pending ? '…' : result?.items.join(', ')}
</>
);
}
```
+ ```tsx
+ const { execute } = useExecutePromise<Order>();
+ const onSubmit = async () => {
+ const { status, error } = await execute(ac => api.placeOrder(form, ac));
+ if (status === 'success') navigate('/done');
+ else if (status === 'error') toast(error?.message);
+ };
+ ```
+
## References
- - `references/api.md`: hook signatures and return fields, `PromiseStatus` transitions, debounced options, storage and event hooks, API hook generation and CoSec components. Load it for exact options.
+ - `references/api.md`: hook signatures and return fields, `PromiseStatus` transitions, cancellation rules, debounced hooks, storage and event hooks, API hook generation and CoSec components. Load it for exact options.
## Related Skills
- $fetcher-integration: the Fetcher, interceptors and result extractors underneath.
- $fetcher-decorator-service: services that `createExecuteApiHooks` / `createQueryApiHooks` wrap.
- $fetcher-storage: `KeyStorage` behind `useKeyStorage`.
- $fetcher-eventbus: buses behind `useEventSubscription`.
- $fetcher-cosec-auth: tokens behind `SecurityProvider`.
- - $fetcher-v6-migration: hooks removed in 6.0.
+ - $fetcher-v6-migration: upgrading 5.x hook code and hooks removed in 6.0.