Synapsev6.0.0

useApiQuery — React hook for GET requests

TL;DR: useApiQuery(endpoint, params, options?) is a React Query–style GET hook. It starts on mount, re-runs when params change, and returns { data, isLoading, isError, error, refetch, fromCache }.

A React Query–style hook over an ApiClient endpoint for reading data (GET). It is a thin layer on top of endpoint.request(params).subscribe(...) — deduplication, the tag cache, retry and abort already live in the endpoint (see ApiClient). The hook adds the React part: subscription, a stable params key, enabled/refetch, an SSR fast-path (no loading flash) and auto-refetch on cache invalidation.

The ApiClient is standalone (it depends neither on RxJS/reactive nor on createSynapse), so you can use just synapse-storage/api + synapse-storage/core + these hooks — without the whole state-manager.

When to use / when you don't need it

  • Use it when a component reads data (GET) and you want loading/error states, caching, auto-refetch after mutations, and an instant first render under SSR — all out of the box.

  • Don't use it for writes (POST/PUT/DELETE/PATCH) — that's useApiMutation.

  • Don't use it outside React: if the data is fetched by an effect/service, work with the native endpoint.request(...) directly (see ApiClient).

Import

typescript
import { useApiQuery } from 'synapse-storage/react'

Usage

The hook takes an endpoint (from getEndpoints()), so params/data types are inferred from it.

typescript
const endpoints = pokemonApiClient.getEndpoints()

function PokemonCard({ id }: { id: number }) {
  // GET: starts on mount, re-runs when params change
  const { data, isLoading, isError, error, refetch, fromCache } = useApiQuery(endpoints.getDetails, { id })

  if (isLoading) return <Spinner />
  if (isError) return <Error message={error?.message} />

  return (
    <div>
      <h3>{data?.name}</h3>
      {fromCache && <small>from cache</small>}
      <button onClick={refetch}>Refresh</button>
    </div>
  )
}

Return value

useApiQuery(endpoint, params, options?) returns:

FieldTypeDescription
dataTData | undefinedResponse data (or cached data)
errorError | undefinedRequest error
status'idle' | 'loading' | 'success' | 'error'Current status
isLoadingbooleanstatus === 'loading'
isErrorbooleanstatus === 'error'
isSuccessbooleanstatus === 'success'
fromCachebooleanData came from cache rather than the network
refetch() => voidForce a re-request

Options (commented)

The third argument is UseApiQueryOptions: two hook-specific fields plus the endpoint's entire QueryOptions.

typescript
useApiQuery(endpoints.getDetails, { id }, {
  // --- hook fields ---
  enabled: id != null,       // false → the request is not performed (lazy). Wait until params are ready
  refetchOnInvalidate: true, // auto-refetch an active query when a mutation invalidates its tags

  // --- forwarded QueryOptions (same as for endpoint.request(...)) ---
  disableCache: false,       // true → bypass the app cache, always a network request
  timeout: 5000,             // timeout for this request (ms), overrides baseQuery.timeout
  signal: controller.signal, // external AbortSignal (the hook already aborts the request on unmount)
  headers: new Headers(),    // extra request headers
  context: { source: 'ui' }, // passed into baseQuery.prepareHeaders
  retry: { count: 2 },       // retry policy for this request (overrides the endpoint/global one)
})
FieldTypeDefaultDescription
enabled?booleantruefalse — the request is not performed (lazy) until params are ready.
refetchOnInvalidate?booleantrueAuto-refetch when a mutation invalidates the endpoint's tags.
disableCache?booleanBypass the app cache (a forced network request).
timeout?numberRequest timeout (ms), overrides baseQuery.timeout.
signal?AbortSignalExternal abort signal.
headers?HeadersExtra request headers.
retry?RetryConfigRetries for this request (see ApiClient).
typescript
// enabled: won't fire until `id` is defined
const { data } = useApiQuery(endpoints.getDetails, { id: id! }, { enabled: id != null })

SSR: no loading flash after hydration

The lazy initial state reads the cache synchronously via endpoint.getCachedSync(). On the server useEffect doesn't run, so the very first (and only) render returns the seeded/cached data; on the client the first render after hydration shows the server data immediately instead of flashing loading.

This works only for synchronous storages (MemoryStorage/LocalStorage) and endpoints without cache-affecting headers. Otherwise the hook falls back to the regular async path.

Auto-refetch on cache invalidation

When a mutation succeeds with invalidatesTags, the matching cache entries are removed and an invalidation event is emitted. An active useApiQuery whose endpoint tags intersect the invalidated tags re-runs automatically (parity with React Query — the query "comes alive" instead of waiting for the TTL). Under the hood this uses endpoint.onCacheInvalidate(). Turn it off with refetchOnInvalidate: false.

typescript
// getList endpoint: tags: ['PokemonList']
const list = useApiQuery(endpoints.getList, { limit: 12 })

// elsewhere — a mutation with invalidatesTags: ['PokemonList']
// → `list` refetches on its own

Notes

  • Stable params key. Params are serialized with sorted keys, so a fresh { id: 1 } object on every render does not cause an infinite re-request.

  • StrictMode-safe. The effect aborts the in-flight request on cleanup.

  • params identity doesn't matter. You can pass an inline object literal — only the serialized key drives re-fetching.

See also

  • ApiClient — the native client, endpoints, caching and SSR.

  • useApiMutation — the companion hook for writes.