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

@firsthandjs/data

v0.12.2

Published

Resources, actions and invalidation for Firsthand: reactive async state, with the cache one layer down.

Readme

@firsthandjs/data

Data that comes from outside the reactive graph, brought in as reactive state: what is loaded, what state that is in, and when it has to be loaded again.

Documentation: guide · API reference · ADR-0022 · ADR-0023

npm install @firsthandjs/data

3.84 kB gzip. It depends on @firsthandjs/core and @firsthandjs/dom.

const api = createFetchClient({ baseUrl: '/api' });

const user = useResource(({ request, tags }) => {
  tags(tag('user', { id: props.id }));
  return api.get<User>(`/users/${props.id}`)(request);
});

return <h1>{user.data.value?.name ?? '…'}</h1>;

No key, no name, no variables object. props.id is read inside the loader, so changing it runs the loader again and aborts what was in flight — exactly as an effect behaves, and for the same reason.

A server is the common case, not the only one: a worker, IndexedDB, a WebSocket, a computation too expensive to repeat are all the same shape.

Two layers

Reactivity — useResource, useAction, tags — knows who is watching what, what state it is in, and when it must run again. Transport — a client — knows how to send one request. Between them is one object:

type Loader<T> = (request: { signal: AbortSignal; force: boolean }) => Promise<T>;

signal ends a request nobody wants. force says this run exists because something was invalidated, so a cache may not answer it. That is the whole contract, which is why a loader can be any function at all.

The cache is the transport's

A cache answers "have I got this already?", which needs to know when two things are the same thing. A resource belongs to its call site, so that knowledge does not exist in the reactivity layer — but at the transport it is right there in the request.

const cache = createCacheClient({ ttl: 30_000 });

// In front of a client…
const api = createFetchClient({ baseUrl: '/api', cache });

// …or in front of anything else, which is the other half of what it is for.
const report = useResource(({ request }) =>
  cache.read(`report:${month.value}`, () => buildReport(month.value))(request),
);

One cache, both jobs. With no ttl it still shares what is in flight — ten components asking at once make one request, which is waste removed rather than staleness introduced. force drops the entry, which is how an invalidation reaches all the way down.

An action never touches it — not read from, not written to, whatever its method or key. A cache holds representations, and what an action gets back is the answer to doing something.

A key is an identity and a request: GET /api/me is the same URL for everybody, so the identity — the authorization header by default, or a scope function you give — is what stops one account being served the answer cached for another.

Tags are for invalidation

They may be coarse, they may overlap, and two unrelated sources may share one. Nothing is ever looked up by them, which is what makes that safe.

const rename = useAction((name: string, { request, invalidates }) => {
  invalidates(tag('user', { id: props.id }), tag('users'));
  return api.patch<User>(`/users/${props.id}`, { json: { name } })(request);
});

tags() replaces, so a run says what it is about rather than accumulating what it used to be about — and it may be called after the answer, for the case where only the server knows which user /users/me was.

Bringing a client

createFetchClient is a small REST client on the browser's own fetch: a base URL, headers read per request so a token may change, a failed status thrown, the abort signal wired through, and the cache.

For anything else, @firsthandjs/data-axios, -urql and -apollo bind an instance you built: your interceptors, your links, your authentication. None of them depends on the client it binds, so none has a version to follow.

For GraphQL, tags come out of the .gql file as @tag / @invalidates directives, read at build time by @firsthandjs/data/vite and typed by @firsthandjs/data/codegen. The document that reaches your client is a plain object with the directives already removed.

What it deliberately does not do

Interceptors, retries, backoff, token refresh, progress events, XSRF, normalisation, optimistic cache surgery, pagination helpers. Each belongs to a transport or to an application's own policy — and a loader takes any client, because it takes any function.

MIT licensed.