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

@ramonda/query

v0.9.1

Published

Async state for Ramonda: cached, deduped, race-free queries and mutations that survive a server render.

Readme

@ramonda/query 🌸

Async state for Ramonda: cached, deduplicated, race-free queries and mutations that survive a server render.

npm license

Status: 0.x. The API changes freely between releases while the design is being explored; from 1.0 the interfaces hold. See the root README.

import { Component } from "@ramonda/core";
import { Query, QueryClientProvider } from "@ramonda/query";
import type { FetchContext } from "@ramonda/query";

class App extends Component {
  private query = this.use(QueryClientProvider, () => ({
    defaults: { staleTime: 30_000 },
  }));

  render() {
    return <UserCard id="42" />;
  }
}

class UserCard extends Component<{ id: string }> {
  private user = this.use(Query, (self: UserCard) => ({
    key: ["user", self.props.id],
    fetch: ({ signal }: FetchContext) => api.getUser(self.props.id, { signal }),
  }));

  render() {
    if (this.user.isPending) return <p>Loading…</p>;
    if (this.user.isError) return <p>Could not load this user.</p>;
    return <p>{this.user.data?.name}</p>;
  }
}

TData comes from fetch — nothing is declared at the call site.

What it does

  • One request per key. Three components asking for ["user", 7] in one render make one request and cannot disagree about the answer.
  • Server rendering with no wiring. A query's data travels to the client inside the hook's own @state, which core already serializes — so there is no dehydrate() to call, no boundary component, and no script tag to place. The first client render shows what the server rendered, and does not fetch it again.
  • Race-free. A key that changes, a manual write, an observer that unmounts — each abandons the request it supersedes, and an abandoned response can never land over newer data.
  • Stale-while-revalidate. Cached data is shown immediately and refreshed in the background. A failed refresh keeps the last known value on screen rather than blanking it to prove the network broke.
  • Triggers you can turn off. Window focus and reconnect refresh only STALE data; refetchInterval polls; all three are options, on the query or on the provider.
  • Mutations with rollback. onMutate returns the undo, and it runs if the write fails — the only place in Ramonda where returning a function means "call this later", because here the later is a specific event rather than an unspecified teardown.

The cache belongs to the tree

There is no global client to import. Query data is per-request state — whose user, whose permissions — and a module is shared by every request a server handles at once, so a module-level cache would serve one visitor's data to another: intermittently, invisibly in development, and only under real traffic. So a QueryClientProvider owns the cache and it reaches components through context, exactly as the router owns route state.

Typing the callbacks

this.use(Query, () => ({ … })) infers TData from fetch, but the props object is what the type is inferred FROM — so a callback parameter left unannotated has no contextual type. Either annotate it, as above, or name the data type on the hook and get every parameter typed:

private todo = this.use(Query<Todo, readonly ["todo", number]>, (self: TodoCard) => ({
  key: ["todo", self.props.id] as const,
  fetch: ({ signal, key }) => api.getTodo(key[1], { signal }),  // both typed
}));

Query<Todo> is an instantiation expression — ordinary TypeScript, and it compiles away. Naming the types does the most for a mutation, where onSuccess, onError and onSettled would each need an annotation otherwise, and where it types mutate's own parameter as well.

Mutations

class AddTodo extends Component {
  private add = this.use(Mutation<Todo, string>, () => ({
    mutate: (title) => api.createTodo(title),
    onMutate: (title, { client }) => {
      const previous = client.peek<Todo[]>(["todos"])?.data;
      // A stand-in for what the server will send back; the refetch replaces it with the real one.
      const optimistic: Todo = { id: `pending:${title}`, title };

      client.setData<Todo[]>(["todos"], (todos) => [...(todos ?? []), optimistic]);
      return () => client.setData(["todos"], previous);   // the rollback
    },
    invalidates: [["todos"]],
  }));

  render() {
    return <button disabled={this.add.isPending}>Add</button>;
  }
}

mutate never rejects — the failure is this.add.error, so a click handler does not have to catch. mutateAsync rejects, for a caller that needs to know.

Options

| Option | Default | What it does | |---|---|---| | staleTime | 0 | How long data counts as fresh | | gcTime | 5 min | How long an unwatched entry is kept | | retry | 3 | Attempts after the first failure; a predicate decides per error | | retryDelay | backoff | 1s, 2s, 4s… capped at 30s | | refetchOnMount | "stale" | "always" | false — data from the server counts as fresh | | refetchOnWindowFocus | true | Refresh stale data when the tab regains focus | | refetchOnReconnect | true | Refresh stale data when the browser comes back online | | refetchInterval | off | Poll every N ms, staleness ignored | | enabled | true | Hold the query back — better than a key with a hole in it |

Set them on a query, or on the provider as defaults for the whole tree.

License

MIT © Nikola Blagojević