npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pyreon/query

v0.19.0

Published

Pyreon adapter for TanStack Query

Downloads

1,322

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-core

Quick 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 refetch

useMutation(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. Wrap QueryErrorResetBoundary around a QuerySuspense + ErrorBoundary pair so a sibling ErrorBoundary recovery automatically clears errored queries. useQueryErrorResetBoundary() returns { reset } for imperative access from inside an ErrorBoundary fallback outside the render-prop form.

useSubscription(options)

Reactive WebSocket with auto-reconnect and QueryClient cache integration. onMessage receives the active QueryClient so push updates can invalidate or directly patch cached queries. Exponential backoff on reconnect (default 1s doubling, max 10 attempts). url and enabled may be signals.

Returns: UseSubscriptionResult with status (signal), send(data), close(), reconnect().

const sub = useSubscription({
  url: 'wss://api.example.com/feed',
  onMessage: (event, client) => {
    if (JSON.parse(event.data).type === 'post-created') {
      client.invalidateQueries({ queryKey: ['posts'] })
    }
  },
})

useSSE(options)

Reactive Server-Sent Events hook — same pattern as useSubscription but read-only. parse deserializes each event; events filters named event types. Updates lastEventId() on every incoming id field so consumers can persist + resume across remount.

Returns: UseSSEResult<T> with data (signal of last parsed message), status (signal), error (signal), lastEventId(), readyState(), close(), reconnect().

const sse = useSSE({
  url: '/api/events',
  parse: JSON.parse,
  onMessage: (data, queryClient) => {
    if (data.type === 'order-updated') {
      queryClient.invalidateQueries({ queryKey: ['orders'] })
    }
  },
})

Resuming across remount. Browser EventSource only auto-resumes (sends the Last-Event-ID header) within a single instance lifetime. A new instance — including a fresh hook call after navigation — starts from scratch. To persist the resume point, pair useStorage with the initialLastEventId option and put the ID in the URL so the server reads it as a query param:

const lastId = useStorage('chat-last-id', '')

const sse = useSSE({
  url: () => `/api/events?lastId=${lastId() || ''}`,
  initialLastEventId: lastId,
  onMessage: (msg) => lastId.set(msg.id),
})

initialLastEventId is read once at mount. Subsequent changes to the seed accessor are intentionally ignored — use the reactive url (or call reconnect()) for runtime overrides.

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 queries

Patterns

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 fetch

Re-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 UseQueryResult is an independent signal. Reading query.data() does not re-run when isFetching changes, and vice versa.
  • useQuery options must be a function () => opts, not a plain object. This is required for reactive option tracking. Changing a signal inside the function re-evaluates options + refetches.
  • useMutation options are a plain object (not a function) — mutations are imperative, there are no reactive queryKeys to re-evaluate, so the function-wrapper overhead would add no value.
  • mutate() swallows errors into the error signal. Use mutateAsync() if you need try/catch.
  • useQueries accepts a function too — pass useQueries(() => ids().map(...)) for reactive query lists. A static array loses tracking.
  • useSuspenseQuery / useSuspenseInfiniteQuery require a QuerySuspense boundary — without it, the narrowed data: Signal<TData> type lies: data() CAN be undefined during the first render cycle.
  • QuerySuspense children must be a function {() => <UI />} — plain JSX children evaluate eagerly, defeating the Suspense gate.
  • useSubscription onMessage runs on every WebSocket frame — debounce cache invalidations for high-frequency streams.
  • useSSE requires parse for typed data — without it, data() is raw string (the event payload as-is).
  • Observer subscriptions are cleaned up automatically on component unmount via onUnmount.