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

@dts/vue-uquery

v0.1.6

Published

Vue micro query — ~4KB gz query/infinite-query/mutation composables for Vue 3, with reactive optimistic updates and SSR hydration.

Readme

@dts/vue-uquery

Vue 3 query, infinite-query, and optimistic mutations in ~2.7 KB gz for typical use. Native Vue reactivity, no adapter layer.

  • Tiny. ~2.7 KB gz for a query-only app (measured via a synthetic consumer bundle against dist/, not aspirational). ~4.2 KB gz worst-case if you import every named export. sideEffects: false and a separate @dts/vue-uquery/extras entry guarantee apps pay only for what they import — verified in CI on every commit.
  • Vue-native. useQuery returns a reactive() object backed by refs; <script setup> await integrates with <Suspense>; reactive keys flow through MaybeRefOrGetter<QueryKey> so a ref in a parent's prop reruns the fetch without remounting.
  • Type-safe end to end. defineQuery({ key, fetcher }) binds the key, fetcher, and value type in one place. The same definition flows through useQuery, client.getQueryData, client.patch, client.invalidateQueries — every consumer infers the same T.
  • Race-safe optimistic mutations. client.patch(key) opens a Vue reactive draft; mutate it in place and every live useQuery updates instantly. Concurrent fetches, entry evictions, and sibling patches can't silently corrupt the cache: commit/revert detect overlap and fall back to invalidate-from-server when local rollback can't be trusted. Pure all-commit overlaps pay nothing.
  • Strict QueryKey. Compile-time-enforced JSON-shaped keys (string | number | nested arrays/objects). Date, Map, Set, BigInt, Symbol, null, undefined are rejected at the type level — no more silent hash collisions.
  • SSR built in. dehydrate(client) / hydrate(client, payload) are free functions exported from the main entry; tree-shake out cleanly for apps that don't do SSR.

A tiny alternative to @tanstack/vue-query if you're on Vue 3 and want the API surface area, not the framework underneath it.

Install

pnpm add @dts/vue-uquery
# or: npm install @dts/vue-uquery / yarn add @dts/vue-uquery

Peer dep: vue ^3.5.

Quick start

// main.ts
import { createApp } from "vue";
import { createQueryClient } from "@dts/vue-uquery";
import App from "./App.vue";

const client = createQueryClient({ staleTime: 5_000 });
createApp(App).use(client).mount("#app");
// queries.ts — define once, use everywhere. The key, fetcher, and result
// type are bound together; `T` flows automatically through every consumer.
import { defineQuery, type MaybeRefOrGetter, toValue } from "@dts/vue-uquery";

interface User {
  id: string;
  name: string;
}

export const userQuery = (id: MaybeRefOrGetter<string>) =>
  defineQuery({
    key: () => ["users", "detail", toValue(id)],
    fetcher: async ({ signal }): Promise<User> =>
      (await fetch(`/api/user/${toValue(id)}`, { signal })).json(),
    staleTime: 10_000,
  });
<!-- User.vue — mount the parent in <Suspense> so the await resolves -->
<script setup lang="ts">
import { toRef } from "vue";
import { useQuery } from "@dts/vue-uquery";
import { userQuery } from "./queries";

const props = defineProps<{ id: string }>();
// Pass the ref, not props.id directly — the key is reactive and the
// fetch re-fires when the parent changes the id prop.
const q = await useQuery(userQuery(toRef(props, "id")));
</script>

<template>
  <p v-if="q.isLoading">Loading…</p>
  <p v-else-if="q.error">{{ String(q.error) }}</p>
  <article v-else>{{ q.data.name }}</article>
</template>
<!-- Parent.vue — Suspense lets the User.vue await resolve before render -->
<Suspense>
  <User :id="userId" />
  <template #fallback><p>Loading…</p></template>
</Suspense>

Don't destructure. Returns are plain reactive objects (Vue reactive() with getters); const { data } = q snapshots and breaks reactivity. Use q.data directly.

API

Core (always loaded)

  • createQueryClient(opts?) — make a client. opts.staleTime, opts.gcTime.
  • useQueryClient() — resolves the active client via inject. Throws if no client was installed via app.use(client) or __setGlobalClient(...) for imperative/test use; the library refuses to auto-create a fallback because that silently splits state across an unprovided client and an injected one when an await boundary loses the component instance.
  • defineQuery({ key, fetcher, ... }) / defineInfiniteQuery({ ... }) — bind a key, fetcher, and result type together. Pass the returned def to useQuery / useInfiniteQuery and the client's imperative methods; T is inferred once and threaded through every consumer. Recommended over raw keys for anything beyond a one-off.
  • client.getQueryData(def-or-key) / client.setQueryData(def-or-key, updater).
  • client.patch<T>(def-or-key) / client.patchEach<T>(prefix, fn) — see the Optimistic updates section below.
  • client.invalidateQueries(def-or-key?, { exact? }) / client.removeQueries(...).
  • client.fetchQuery(def | { key, fetcher }) — imperative one-shot.
  • dehydrate(client) / hydrate(client, payload) — free functions; see SSR below. Tree-shake out when not imported.
  • hashKey(key) — sorted-JSON canonical hash for a QueryKey.

QueryKey is string | readonly QueryKeyValue[], where QueryKeyValue is string | number | nested arrays/objects — strict at the type level. Any value that doesn't JSON-stringify cleanly (Date, Map, Set, BigInt, Symbol, null, undefined, functions) is rejected by TypeScript.

Composables

// useQuery
const q = await useQuery<T>({
  key,           // string | readonly unknown[] | ref/getter of one
  fetcher,       // (signal) => Promise<T>
  enabled?,      // ref/getter — gate fetching
  staleTime?,    // ms before background refetch (default 0)
  gcTime?,       // ms after last unsubscribe before eviction (default 5min)
  meta?,         // arbitrary record, surfaced on the cache entry
  awaitInitial?, // default true; set false to skip the SSR await
  retry?,        // number of retries (default 0)
  retryDelay?,   // ms or (attempt) => ms; default exponential 1s→30s cap
  shouldRetry?,  // (err, attempt) => boolean
});
// q.data, q.error, q.status, q.isLoading, q.isFetching, q.refetch()

// useInfiniteQuery
const list = await useInfiniteQuery<TPage, TPageParam>({
  key, initialPageParam, fetcher: ({ pageParam, signal }) => ...,
  getNextPageParam: (lastPage, allPages) => nextOrUndefined,
  // + same staleTime/gcTime/meta/enabled/awaitInitial/retry as above
});
// list.pages, list.pageParams, list.hasNextPage, list.fetchNextPage(),
// list.isFetching, list.isFetchingNextPage, list.refetch()

// useMutation — synchronous; no `await` needed.
const m = useMutation<T, V>({
  mutator: async (vars) => ...,
  scope?,                       // used by useIsMutating({ scope }) extra
  // onMutate receives a `patch` helper that opens a mutable draft on a
  // cache key. Every patch taken here is auto-committed on success and
  // auto-reverted on rejection — no `onError` boilerplate needed for the
  // common "snapshot + rollback" case.
  onMutate?: (vars, { patch, patchEach }) => ...,
  onSuccess?, onError?, onSettled?,
});
// m.data, m.error, m.status ('idle'|'pending'|'success'|'error'), m.reset()
// m.mutate(v): Promise<T>  — rejects on mutator/onError throws; catch or
//                           ignore depending on whether the UI tracks status.

Invalidation

client.invalidateQueries(def-or-key?, { exact? }) marks matching entries stale and triggers a refetch on those that have an active observer. Use it when an event happens that makes cached data no longer trustworthy — typically a mutation with server-side side effects you can't compute locally, a manual "refresh" button, or focus/reconnect.

client.invalidateQueries(["users"]); // every "users.*" entry
client.invalidateQueries(["users", "detail", "1"], { exact: true }); // exact match only
client.invalidateQueries(userQuery(id)); // typed via def
client.invalidateQueries(); // every entry

You usually don't need to invalidate inside useMutation. Patches opened in onMutate auto-commit on the mutator's success and auto-revert on its failure; when local rollback would be ambiguous (concurrent fetch, sibling patch, evicted entry) the patch system already hands off to the server (mark stale + refetch). Reach for an explicit onSettled: () => client.invalidateQueries(...) only when the mutation triggered server-side state that doesn't map to your optimistic write — derived counts, audit logs, related resources updated by a webhook.

client.removeQueries(def-or-key?, { exact? }) is the harder hammer: drop matching entries from the cache entirely. Pair it with logout flows or "discard everything for this account" buttons.

Optimistic updates

The cache exposes a patch primitive built around Vue's reactivity rather than React-style snapshot-and-restore. Standalone (e.g. from a button handler outside useMutation):

const user = client.patch<User>(["users", "detail", id]);
// → Patch<User> | undefined  (undefined if the entry isn't loaded yet, or
//    holds a primitive / non-cloneable value)
if (user) {
  user.draft.name = "New"; // live useQuery views update immediately
  try {
    await api.updateUser(...);
    user.commit();         // freeze the optimistic value in cache (bails
                           //   harmlessly if the cache moved during the
                           //   await — see Race safety below)
  } catch (e) {
    user.revert();         // roll back to the pre-mutation snapshot, OR
                           //   invalidate-from-server if rollback can't be
                           //   trusted (concurrent fetch / sibling patch)
    throw e;
  }
}

Inside useMutation.onMutate the lifecycle is automated — the helper returns the draft directly (no Patch wrapper, no manual commit/revert), because every patch taken in onMutate is auto-committed on success and auto-reverted on rejection. No onSettled: () => invalidateQueries(...) boilerplate needed for the common "edit one record, optimistically reflect in detail + list views" case:

const update = useMutation<User, string>({
  mutator: (name) => api.updateUser(id, { name }),
  onMutate: (name, { patch, patchEach }) => {
    // Pass the def — draft is typed as `User`, no manual <User> needed.
    const u = patch(userQuery(id));
    if (u) u.name = name;
    // Same for `patchEach`: pass the infinite-list def and the draft is
    // typed as `{ pages: Page<User>[], pageParams: number[] }`. No raw
    // key prefix, no `<{ pages: { items: User[] }[] }>` widening.
    patchEach(userListQuery(), (list) => {
      for (const page of list.pages)
        for (const item of page.items) if (item.id === id) item.name = name;
    });
  },
  // No onSettled / onSuccess / onError needed for the rollback or refetch.
  // Add onSuccess only for stuff like router navigation or toast UI; add
  // an explicit invalidateQueries only if the server changes data your
  // optimistic write didn't cover (counts, audit logs, related entities).
});

patch requires the entry to already exist in the cache; non-object values (primitives) and non-cloneable objects (records with own-property functions) return undefined so onMutate can't crash a mutation. For cases where you want to create-or-update, fall back to client.setQueryData(key, value) — that path doesn't snapshot/auto-revert.

Race safety. commit / revert check that the cache still holds the proxy they installed. If something else has touched the entry during the mutator's await (a concurrent fetch, a sibling patch, an eviction), the settle paths bail or hand off to the server instead of clobbering:

  • Two mutations on the same key both succeeding → both values land via the natural stale-bail; no refetch fires.
  • Any revert during overlap → mark stale + refetch if observed (fire-and-forget; useMutation's lifecycle callbacks fire on the mutator promise, independent of the patch's settle path).
  • Concurrent fetch lands during an optimistic window → the fresh value is preserved on commit-bail; revert hands off to the server.

The mental model: the patch system handles the rollback/recovery ambiguity for you. Reserve client.invalidateQueries(...) for the cases described under Invalidation above — server-side state your optimistic write can't compute.

Extras — @dts/vue-uquery/extras

Tree-shakable opt-in helpers:

import {
  // fetcher wrappers
  structuralShare,
  withStructuralShare,
  // result views
  placeholder,
  select,
  throwOnError,
  // client utilities
  cancelQueries,
  matchQueries,
  useIsFetching,
  useIsMutating,
  seedQuery,
  // lifecycle effects
  refetchOnFocus,
  refetchOnReconnect,
  useRefetchInterval,
  persist,
  useOfflinePause,
} from "@dts/vue-uquery/extras";

| Helper | Purpose | | ------------------------------------------ | -------------------------------------------------------- | | placeholder(q, { keepPrevious }) | Show prior data during transitions | | select(q, fn) | Derive a transformed view of q.data | | throwOnError(q) | Re-throw q.error for an error boundary | | cancelQueries(client, key?) | Abort in-flight fetches matching a key prefix | | matchQueries(client, predicate) | Find cache entries by arbitrary predicate | | useIsFetching(key?) | Reactive count of in-flight queries | | useIsMutating(scope?) | Reactive count of pending mutations | | seedQuery(client, key, data, updatedAt?) | Pre-populate the cache | | withStructuralShare(fetcher, current) | Preserve object identity for unchanged sub-trees | | refetchOnFocus(client, key?) | Invalidate matching queries on window.focus/visibility | | refetchOnReconnect(client, key?) | Invalidate matching queries when network returns | | useRefetchInterval(query, ms) | Reactive polling cadence; 0 stops | | persist(client, storage, opts?) | Hydrate from + write to localStorage-shaped storage | | useOfflinePause(client) | Cancel fetches offline; invalidate when back online |

SSR

// server
import { createQueryClient, dehydrate } from "@dts/vue-uquery";
const client = createQueryClient();
await client.fetchQuery(userQuery("1"));
const payload = dehydrate(client); // ship in <script> tag

// client
import { createQueryClient, hydrate } from "@dts/vue-uquery";
const client = createQueryClient({ staleTime: 60_000 });
hydrate(client, payload);
// useQuery for the same key returns immediately without a fetch.

dehydrate/hydrate are free functions over the client (not methods on it) so an app that doesn't SSR never pays for them — sideEffects: false guarantees the bundler drops them.

Bundle size

Two numbers, two audiences, both measured in CI on every commit:

| Number | Meaning | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ~2.7 KB gz | Typical consumer: a synthetic build importing only createQueryClient, useQuery, useQueryClient against dist/. What your users actually load if you're query-only. | | ~4.4 KB gz | Worst case if a bundler does no tree-shaking. Every named export from the main entry — define helpers, mutations, infinite, SSR, the patch system, all in. |

scripts/check-bundle-size.mjs builds a synthetic consumer probe with the same Vite + rolldown the rest of the toolchain uses; the budget is enforced so a refactor can't drift these numbers without a test failure.

@dts/vue-uquery/extras is a separate entry on the same terms — every helper you don't import tree-shakes out.

What it doesn't have (and won't)

  • No useQueries — dynamic-array parallel queries. Out of scope for the core budget; a candidate for a future extra.
  • No DevTools panel. The cache exposes client.cache.entries and a reactive cache.version ref, so a devtools layer is implementable in extras, but nothing ships in the box.
  • No global mutation callbacks. Per-mutation onSuccess / onError / onSettled only — for global error logging, wrap useMutation in your own composable.
  • No queueing or mutationKey scopes. Concurrent mutations on the same key are race-safe (see above), but there's no built-in serialization.

For everything else (retries with backoff, window-focus refetch, polling, placeholder/keepPrevious, structuralSharing, persistence, offline pause, etc.) see the extras subpath.

The general design principle: the core exposes reactive primitives; everything else is Vue composition. If a feature can be implemented by watch/computed/reactive over the existing cache + mutation refs, it belongs in extras or in your own application code — not in the core.

Releasing

The example app and any future workspace package reference the library as "@dts/vue-uquery": "workspace:^". pnpm rewrites that to the concrete ^X.Y.Z range at publish time, so internal references stay version-free.

To cut a release:

pnpm run release patch        # 0.0.1 -> 0.0.2
pnpm run release minor        # 0.0.1 -> 0.1.0
pnpm run release prerelease --preid=beta
pnpm run release 0.2.0-alpha.0

scripts/deploy.mjs enforces a clean tree on master, bumps package.json, publishes (running the full check pipeline via prepublishOnly), commits the bump, and creates an annotated git tag (vX.Y.Z). Pre-release versions get a matching npm dist-tag so npm install @dts/vue-uquery keeps pointing at the stable latest. After it succeeds, push:

git push origin master --follow-tags

License

MIT © Daniel Staudigel