@pyreon/query
v0.12.10
Published
Pyreon adapter for TanStack Query
Readme
@pyreon/query
Pyreon adapter for TanStack Query. Reactive useQuery, useMutation, useInfiniteQuery, and Suspense integration with fine-grained signal updates.
Install
bun add @pyreon/query @tanstack/query-coreQuick Start
import { QueryClient, QueryClientProvider, useQuery } from '@pyreon/query'
const queryClient = new QueryClient()
function UserProfile(props: { id: string }) {
const query = useQuery(() => ({
queryKey: ['user', props.id],
queryFn: () => fetch(`/api/users/${props.id}`).then((r) => r.json()),
}))
return () => {
if (query.isLoading()) return <p>Loading...</p>
if (query.isError()) return <p>Error</p>
return <h1>{query.data()?.name}</h1>
}
}
const App = () => (
<QueryClientProvider client={queryClient}>
<UserProfile id="1" />
</QueryClientProvider>
)API
QueryClientProvider
Provide a QueryClient to the component tree.
| Parameter | Type | Description |
| --------- | ------------- | ------------------------------ |
| client | QueryClient | TanStack Query client instance |
useQueryClient()
Access the QueryClient from the nearest QueryClientProvider.
Returns: QueryClient
useQuery(options)
Subscribe to a query with fine-grained reactive signals. Options are passed as a function so reactive values (e.g. signal-based query keys) trigger automatic refetches.
| Parameter | Type | Description |
| --------- | ---------------------------- | -------------------------------- |
| options | () => QueryObserverOptions | Function returning query options |
Returns: UseQueryResult<TData, TError> with:
| Property | Type | Description |
| ------------ | ------------------------------------------- | ----------------------- |
| result | Signal<QueryObserverResult> | Full observer result |
| data | Signal<TData \| undefined> | Query data |
| error | Signal<TError \| null> | Query error |
| status | Signal<"pending" \| "error" \| "success"> | Query status |
| isPending | Signal<boolean> | No data yet |
| isLoading | Signal<boolean> | First fetch in progress |
| isFetching | Signal<boolean> | Any fetch in progress |
| isError | Signal<boolean> | Query errored |
| isSuccess | Signal<boolean> | Query succeeded |
| refetch() | () => Promise<QueryObserverResult> | Trigger manual refetch |
const userId = signal(1)
const query = useQuery(() => ({
queryKey: ['user', userId()],
queryFn: () => fetchUser(userId()),
}))
// Changing userId triggers automatic refetchuseMutation(options)
Run a mutation with reactive status signals.
| Parameter | Type | Description |
| --------- | ------------------------- | --------------- |
| options | MutationObserverOptions | Mutation config |
Returns: UseMutationResult<TData, TError, TVariables, TContext> with:
| Property | Type | Description |
| -------------------------- | ----------------------------------------------------- | -------------------------- |
| data | Signal<TData \| undefined> | Mutation result |
| error | Signal<TError \| null> | Mutation error |
| status | Signal<"idle" \| "pending" \| "success" \| "error"> | Status |
| isPending | Signal<boolean> | Mutation in progress |
| isSuccess | Signal<boolean> | Mutation succeeded |
| isError | Signal<boolean> | Mutation errored |
| isIdle | Signal<boolean> | Not yet fired |
| mutate(vars, opts?) | Function | Fire-and-forget mutation |
| mutateAsync(vars, opts?) | Function | Promise-returning mutation |
| reset() | () => void | Reset to idle state |
const mutation = useMutation({
mutationFn: (data: { title: string }) =>
fetch('/api/posts', { method: 'POST', body: JSON.stringify(data) }).then((r) => r.json()),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
})
mutation.mutate({ title: 'New Post' })useInfiniteQuery(options)
Paginated/infinite query with the same fine-grained signal pattern as useQuery.
| Parameter | Type | Description |
| --------- | ------------------------------------ | -------------------------- |
| options | () => InfiniteQueryObserverOptions | Function returning options |
Returns: UseInfiniteQueryResult<TData, TError> — same shape as UseQueryResult.
useQueries(options)
Run multiple queries in parallel.
| Parameter | Type | Description |
| --------- | ------------------- | ---------------------- |
| options | UseQueriesOptions | Array of query configs |
Returns: Array of UseQueryResult objects.
useSuspenseQuery(options) / useSuspenseInfiniteQuery(options)
Suspense-enabled queries. Data is guaranteed non-undefined after the suspense boundary resolves.
| Property | Type | Description |
| -------- | --------------- | ------------------------------ |
| data | Signal<TData> | Always defined (non-undefined) |
function UserList() {
const query = useSuspenseQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
return () => (
<ul>
{query.data().map((u) => (
<li>{u.name}</li>
))}
</ul>
)
}QuerySuspense
Suspense wrapper component with built-in error handling.
| Parameter | Type | Description |
| ---------- | ------------ | ---------------- |
| fallback | VNodeChild | Loading fallback |
| children | VNodeChild | Content |
<QuerySuspense fallback={<p>Loading...</p>}>
<UserList />
</QuerySuspense>QueryErrorResetBoundary / useQueryErrorResetBoundary()
Error boundary for resetting query errors on retry.
useIsFetching(filters?) / useIsMutating(filters?)
Global counters of active queries/mutations as reactive signals.
| Parameter | Type | Description |
| --------- | ---------------------------------- | -------------------------------- |
| filters | QueryFilters / MutationFilters | Optional filters to narrow scope |
Returns: Signal<number>
const fetching = useIsFetching()
// fetching() => number of active queriesPatterns
SSR Dehydration
import { QueryClient, dehydrate, hydrate } from '@pyreon/query'
// Server: prefetch and serialize
const queryClient = new QueryClient()
await queryClient.prefetchQuery({ queryKey: ['users'], queryFn: fetchUsers })
const dehydratedState = dehydrate(queryClient)
// Client: restore cache
hydrate(queryClient, dehydratedState)Reactive Query Keys
Options are a function, so reading signals inside auto-tracks dependencies.
const filter = signal('active')
const query = useQuery(() => ({
queryKey: ['todos', filter()],
queryFn: () => fetchTodos(filter()),
}))
// Changing filter() triggers a new fetchRe-exports from @tanstack/query-core
Runtime: QueryClient, QueryCache, MutationCache, dehydrate, hydrate, keepPreviousData, hashKey, isCancelledError, CancelledError, defaultShouldDehydrateQuery, defaultShouldDehydrateMutation
Types: QueryKey, QueryFilters, MutationFilters, DehydratedState, FetchQueryOptions, InvalidateQueryFilters, InvalidateOptions, RefetchQueryFilters, RefetchOptions, QueryClientConfig
Gotchas
- Each field on
UseQueryResultis an independent signal. Readingquery.data()does not re-run whenisFetchingchanges, and vice versa. useQueryoptions must be a function() => opts, not a plain object. This is required for reactive option tracking.useMutationoptions are a plain object (not a function) since mutations are imperative.mutate()swallows errors into theerrorsignal. UsemutateAsync()if you need try/catch.- Observer subscriptions are cleaned up automatically on component unmount via
onUnmount.
