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

@kontsedal/light-query

v0.1.0

Published

A lightweight and dumb react-query alternative

Readme

light-query

npm version gzip size license

A tiny (~2kb gzipped) React data-fetching library. Simple hooks for caching, retries, pagination, and keeping your server state in sync — without the bundle bloat.

Why light-query?

  • Tiny — ~2kb gzipped, zero runtime dependencies
  • Simple API — three hooks cover most data-fetching needs
  • TypeScript-first — full type inference, generic hooks
  • Familiar — if you've used react-query, you already know this

Installation

npm install @kontsedal/light-query
# or
yarn add @kontsedal/light-query
# or
pnpm add @kontsedal/light-query

Quick Start

import { useQuery } from "@kontsedal/light-query";

function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery(
    `user-${userId}`,
    () => fetch(`/api/users/${userId}`).then((r) => r.json())
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading user</div>;
  return <div>{data.name}</div>;
}

API Reference

useQuery

Fetches and caches data. Subsequent calls with the same key return cached data without refetching.

const result = useQuery<T>(key: string, fetchFn: () => Promise<T> | T, options?: UseQueryOptions<T>)

Options

| Option | Type | Default | Description | |---|---|---|---| | enabled | boolean | true | Set to false to disable automatic fetching | | staleTime | number | 0 | Milliseconds before data is considered stale and refetched on next access | | cacheTime | number | 300000 | Milliseconds to keep unused data in cache before garbage collection | | initialData | T | — | Pre-populate cache before first fetch | | refetchInterval | (data?: T) => number \| Promise<number> | — | Return interval in ms. Return 0 to stop polling | | refetchOnWindowFocus | boolean | false | Refetch when browser tab regains focus | | refetchOnReconnect | boolean | false | Refetch when network connection is restored | | retry | (attempt, error, state?) => number \| Promise<number> | — | Return delay in ms. Return 0 to stop retrying | | onSuccess | (data: T) => void | — | Called after a successful fetch | | onError | (error: unknown) => void | — | Called after a failed fetch | | cache | Cache | global | Use a custom cache instance |

Return Value

| Field | Type | Description | |---|---|---| | data | T \| undefined | The fetched data | | error | unknown \| undefined | The error, if the fetch failed | | isLoading | boolean | Currently fetching | | isIdle | boolean | Never fetched yet (!isLoading && !lastFetchedAt) | | isUpdating | boolean | Refetching after a previous successful load | | isSuccess | boolean | Has data, no error, not loading | | isError | boolean | Has error, not loading | | isFetched | boolean | At least one fetch attempt has completed | | lastFetchedAt | number \| undefined | Timestamp of the last fetch attempt | | refetch | () => Promise | Force a refetch, bypassing stale checks | | reset | () => void | Clear cached data, returning to idle state | | setData | (data: T \| ((prev?: T) => T)) => void | Manually update cached data (useful for optimistic updates) | | getData | () => T \| undefined | Read cached data outside of render (avoids stale closures) |


useMutation

A simple wrapper for async side effects (form submissions, API calls, etc.) without caching.

const { isLoading, error, mutate } = useMutation<T, D>(
  mutationFn: (vars: T) => Promise<D> | D
)

When T is void (default), mutate() can be called with no arguments. When T is a specific type, the first argument is required.

Return Value

| Field | Type | Description | |---|---|---| | isLoading | boolean | Currently executing | | error | unknown \| undefined | Error from the last execution | | mutate | (vars: T, throwError?: boolean) => Promise<D> | Execute the mutation. Pass throwError: true to re-throw errors |


usePagination

Built on top of useQuery for managing paginated / infinite query data. Accumulates pages as you navigate, supports both numeric offsets and string cursors.

const result = usePagination<T, D, P = number>(
  key: string,
  fetchFn: (params: D | undefined) => Promise<T> | T,
  options: UsePaginationOptions<T, D, P>
)

Options

Extends all useQuery options, plus:

| Option | Type | Default | Description | |---|---|---|---| | defaultPageId | P | 1 | Initial page identifier | | getFetchPageParams | (requestedPageId, currentPageId, currentPage, allPages) => D \| undefined | required | Maps a page ID to fetch parameters. Return undefined to indicate the page doesn't exist |

The allPages argument is a Map<P, T> containing all fetched pages.

Return Value

All useQuery return fields, plus:

| Field | Type | Description | |---|---|---| | pages | T[] | Array of all fetched pages | | pageId | P | Current page identifier | | hasPage | (pageId: P) => boolean | Check if a page can be fetched | | fetchPage | (pageId: P) => void | Navigate to a page |


createCache

Create a custom cache instance. A global cache is used by default.

const cache = createCache(options?: CreateCacheOptions)

Options

| Option | Type | Default | Description | |---|---|---|---| | staleTime | number | 0 | Default stale time for all queries | | cacheTime | number | 300000 | Default cache lifetime (5 min) | | garbageCollectorInterval | number | 500 | How often to check for expired entries (ms) | | refetchOnWindowFocus | boolean | false | Default for all queries | | refetchOnReconnect | boolean | false | Default for all queries |

Methods

| Method | Description | |---|---| | invalidate(keyOrPrefix) | Invalidate entries by exact key or prefix. Matching queries will refetch on next access | | get(key) | Read the current state for a key | | set(key, values, notify?) | Update state. Set notify: false to skip listener notifications | | toggleGc(enabled) | Enable/disable the garbage collector |


CacheContext / useCache

Provide a custom cache to a component tree via React context.

import { createCache, CacheContext } from "@kontsedal/light-query";

const myCache = createCache({ staleTime: 30000 });

function App() {
  return (
    <CacheContext.Provider value={myCache}>
      <MyComponent />
    </CacheContext.Provider>
  );
}

Advanced Patterns

Retry with Exponential Backoff

const { data } = useQuery("posts", fetchPosts, {
  retry: (attempt, error) => {
    if (attempt >= 3) return 0; // stop after 3 attempts
    return Math.min(1000 * 2 ** attempt, 30000); // 2s, 4s, 8s...
  },
});

Optimistic Updates

const { setData, refetch } = useQuery("todos", fetchTodos);
const { mutate } = useMutation(addTodo);

async function handleAdd(todo) {
  setData((prev) => [...(prev ?? []), todo]); // optimistic update
  try {
    await mutate(todo);
  } catch {
    refetch(); // rollback on failure
  }
}

Cache Invalidation After Mutation

import { globalCache } from "@kontsedal/light-query";

const { mutate } = useMutation(updateUser);

async function handleSave(userData) {
  await mutate(userData);
  globalCache.invalidate("user-"); // invalidate all user-* queries
}

Conditional Fetching

const { data: user } = useQuery("user", fetchUser);
const { data: posts } = useQuery(
  `posts-${user?.id}`,
  () => fetchPosts(user.id),
  { enabled: !!user?.id }
);

Cursor-Based Pagination

type Page = { items: Item[]; nextCursor?: string };

const { pages, pageId, fetchPage, hasPage } = usePagination<Page, string, string>(
  "feed",
  (cursor) => fetchFeed(cursor),
  {
    defaultPageId: "initial",
    getFetchPageParams: (requestedId, currentId, currentPage, allPages) => {
      if (requestedId === "initial") return "";
      // Find the page that has the cursor pointing to the requested page
      for (const [, page] of allPages) {
        if (page.nextCursor === requestedId) return requestedId;
      }
      return undefined; // page not reachable
    },
  }
);

Numeric Pagination

const { data, pages, pageId, fetchPage, hasPage } = usePagination(
  "posts",
  (page) => fetch(`/api/posts?page=${page}`).then((r) => r.json()),
  {
    getFetchPageParams: (requestedPage) => requestedPage,
  }
);

// Navigate
if (hasPage(pageId + 1)) fetchPage(pageId + 1);

Polling

const { data } = useQuery("status", fetchStatus, {
  refetchInterval: (data) => {
    if (data?.status === "completed") return 0; // stop polling
    return 3000; // poll every 3 seconds
  },
});

Comparison with react-query

| Feature | light-query | react-query | |---|:---:|:---:| | Bundle size (gzip) | ~2kb | ~13kb | | Basic caching | Y | Y | | Stale time / cache time | Y | Y | | Retry logic | Y | Y | | Infinite / paginated queries | Y | Y | | Mutations | Y | Y | | Window focus refetch | Y | Y | | Network reconnect refetch | Y | Y | | Cache invalidation | Y | Y | | Optimistic updates | Y | Y | | Initial data | Y | Y | | onSuccess / onError | Y | Y | | DevTools | - | Y | | SSR / Suspense | - | Y | | Structural sharing | - | Y |

light-query is designed for apps that need the core data-fetching primitives without the overhead. If you need advanced features like SSR, Suspense, or DevTools, use react-query.

License

MIT