fetcher-react-hooks · git:20261003.7cd1148 · 2026-10-03 · sha256 20d062e71c3ce94e
fetcher-react-hooks git:20261003.7cd1148A
Immutable. This exact content never changes and is served at /api/v1/blob/20d062e71c3ce94e.
---
name: fetcher-react-hooks
description: >
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/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
- `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 { useState } from 'react';
import { useDebouncedFetcherQuery } from '@ahoo-wang/fetcher-react';
export function Search() {
const [query, setQuery] = useState({ keyword: '' });
const { loading, result, error, pending } = useDebouncedFetcherQuery<
{ keyword: string },
{ items: string[] }
>({ url: '/api/search', query, debounce: { delay: 300 } });
return (
<>
<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, 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: upgrading 5.x hook code and hooks removed in 6.0.