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

@forgedevstack/forge-query

v1.0.2

Published

Powerful data fetching and caching library for React with DevTools support

Readme

Forge Query


Why Forge Query?

| Feature | Forge Query | Others | |---------|-------------|--------| | Bundle Size | ~12KB | 30KB+ | | DevTools | Built-in + Chrome Extension | Separate package | | Setup | 2 lines | 10+ lines | | Learning Curve | Minimal | Steep | | TypeScript | First-class | Added later |

Features

  • Simple API — Just useQuery and useMutation
  • Infinite queriesuseInfiniteQuery with page accumulation
  • PrefetchprefetchQuery, ensureQueryData, prefetchQueries
  • Persistence — optional cache restore via persistQueryClient (JSON-serializable MVP)
  • Smart Caching — Automatic with configurable stale/cache times
  • Background Sync — Auto-refetch on focus, reconnect, and intervals
  • Live QueriesuseSubscription streams WebSocket/SSE events into the cache
  • Optimistic Updates — Declarative optimisticUpdate with automatic rollback
  • DevTools — In-app panel + Chrome extension (optional Bear peer)
  • Tiny core — No UI kit required for the query client
  • Flexible — Works with fetch, axios, graphql, or any async function

Quick Start

1. Install

npm install @forgedevstack/forge-query

Core depends only on React. For in-app DevTools:

npm install @forgedevstack/bear

2. Setup

import { QueryClient, QueryClientContext } from '@forgedevstack/forge-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientContext.Provider value={queryClient}>
      <YourApp />
    </QueryClientContext.Provider>
  );
}

3. Fetch Data

import { useQuery } from '@forgedevstack/forge-query';

function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  });

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  
  return <Profile user={data} />;
}

4. Mutate Data

import { useMutation, useQueryClient } from '@forgedevstack/forge-query';

function CreateTodo() {
  const queryClient = useQueryClient();
  
  const { mutate, isLoading } = useMutation({
    mutationFn: (todo) => fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify(todo),
    }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  return (
    <button onClick={() => mutate({ title: 'New Todo' })} disabled={isLoading}>
      Add Todo
    </button>
  );
}

5. Go Live

import { useSubscription } from '@forgedevstack/forge-query';

function Channel({ channelId }) {
  const { data: messages, isSubscribed } = useSubscription({
    queryKey: ['messages', channelId],
    queryFn: () => fetchMessages(channelId),
    source: {
      subscribe: (listener) => {
        const ws = new WebSocket(`wss://chat.example.com/${channelId}`);
        ws.onmessage = (e) => listener(JSON.parse(e.data));
        return () => ws.close();
      },
    },
    onEvent: (message, messages = []) => [...messages, message],
  });

  return <MessageList messages={messages} live={isSubscribed} />;
}

DevTools

In-App Panel

Add the DevTools component to see all queries, logs, and cache in real-time. Requires optional peer @forgedevstack/bear:

import { ForgeQueryDevTools } from '@forgedevstack/forge-query/devtools';

function App() {
  return (
    <QueryClientContext.Provider value={queryClient}>
      <YourApp />
      <ForgeQueryDevTools />
    </QueryClientContext.Provider>
  );
}

Chrome Extension

Get a dedicated DevTools panel in Chrome:

  1. Download from Chrome Web Store (coming soon)
  2. Or build from source:
    cd devtools-extension
    npm install
    npm run build
  3. Load in Chrome: Extensions → Developer Mode → Load Unpacked

Features:

  • View all queries and their status
  • Activity logs with timestamps
  • Cache statistics and data preview
  • Refetch, invalidate, or remove queries

API Reference

useQuery

const {
  data,         // The fetched data
  error,        // Error if failed
  isLoading,    // Initial loading state
  isFetching,   // Any fetching (including background)
  isError,      // Error state
  isSuccess,    // Success state
  refetch,      // Manual refetch function
} = useQuery({
  queryKey: ['users'],        // Unique cache key
  queryFn: fetchUsers,        // Async function
  staleTime: 60000,           // Time until stale (ms)
  cacheTime: 300000,          // Cache retention (ms)
  retry: 3,                   // Retry attempts
  enabled: true,              // Enable/disable
  refetchOnMount: true,       // Refetch on mount
  refetchOnWindowFocus: true, // Refetch on focus
  refetchInterval: false,     // Auto-refetch interval
});

useMutation

const {
  mutate,       // Trigger mutation
  mutateAsync,  // Returns promise
  data,         // Result data
  error,        // Error if failed
  isLoading,    // Loading state
  reset,        // Reset state
} = useMutation({
  mutationFn: (data) => createItem(data),
  onMutate: (variables) => { /* Before mutation */ },
  onSuccess: (data) => { /* On success */ },
  onError: (error) => { /* On error */ },
  onSettled: () => { /* Always runs */ },
  optimisticUpdate: {                    // Optional declarative optimistic update
    queryKey: ['items'],                 // Cached query to update
    updater: (old, vars) => [...],       // Produce optimistic value
    reconcile: (old, data, vars) => [...], // Merge server data on success (optional)
    invalidateOnSettled: true,           // Refetch after settle (optional)
  },
});

useSubscription

const {
  data,               // Live cached data
  isSubscribed,       // Source is being consumed
  subscriptionStatus, // 'idle' | 'subscribed' | 'closed'
  ...queryResult      // Everything useQuery returns
} = useSubscription({
  queryKey: ['messages', channelId], // Cache entry fed by events
  queryFn: fetchMessages,            // Optional initial fetch
  source: {                          // Any push source adapter
    subscribe: (listener) => {
      // Wire up WebSocket/SSE/emitter, call listener(event) per event.
      // The source owns reconnection — the library only consumes.
      return () => { /* cleanup on unmount */ };
    },
  },
  onEvent: (event, currentData) => nextData, // Fold event into cache
  onEventError: (error) => { /* Reducer threw */ },
});

Outside React, pipe a source into the cache with the client:

const stop = queryClient.consumeSubscription({
  queryKey: ['messages', channelId],
  source: mySource,
  onEvent: (message, messages = []) => [...messages, message],
});

stop(); // Stop consuming

useInfiniteQuery

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: ['projects'],
  initialPageParam: 0,
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});

// data.pages — array of page results
// data.pageParams — params used for each page

Prefetch helpers

await queryClient.prefetchQuery({
  queryKey: ['user', id],
  queryFn: () => fetchUser(id),
});

const user = await queryClient.ensureQueryData({
  queryKey: ['user', id],
  queryFn: () => fetchUser(id),
  staleTime: 60_000,
});

await queryClient.prefetchQueries([
  { queryKey: ['a'], queryFn: fetchA },
  { queryKey: ['b'], queryFn: fetchB },
]);

Cache persistence (MVP)

import {
  QueryClient,
  persistQueryClient,
  createLocalStoragePersister,
} from '@forgedevstack/forge-query';

const queryClient = new QueryClient();

const persistence = persistQueryClient(queryClient, {
  storage: createLocalStoragePersister(),
  persistKey: 'my-app-query-cache',
  buster: 'v1', // bump to invalidate old caches
  maxAge: 24 * 60 * 60 * 1000,
});

await persistence.restore();
// subscribe auto-persists on client updates; call persistence.unsubscribe() on teardown

Limits: JSON-serializable data only; errors / AbortSignals / functions are not restored; no encryption; not a full offline mutation queue.

QueryClient

const queryClient = new QueryClient({
  defaultOptions: {
    staleTime: 0,
    cacheTime: 5 * 60 * 1000,
    retry: 3,
  },
});

// Methods
queryClient.getQueryData(['users']);           // Get cached data
queryClient.setQueryData(['users'], newData);  // Set cache
queryClient.ensureQueryData({ queryKey: ['users'], queryFn: fetchUsers });
queryClient.prefetchQuery({ queryKey: ['users'], queryFn: fetchUsers });
queryClient.invalidateQueries(['users']);      // Mark stale & refetch
queryClient.refetchQueries(['users']);         // Force refetch
queryClient.removeQueries(['users']);          // Remove from cache
queryClient.clear();                           // Clear all

Examples

Dependent Queries

const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: fetchUser,
});

const { data: posts } = useQuery({
  queryKey: ['posts', user?.id],
  queryFn: () => fetchPosts(user.id),
  enabled: !!user, // Wait for user
});

Optimistic Updates

Declarative — snapshot and rollback are automatic:

const { mutate } = useMutation({
  mutationFn: createTodo,
  optimisticUpdate: {
    queryKey: ['todos'],
    updater: (todos = [], newTodo) => [...todos, { ...newTodo, pending: true }],
    reconcile: (todos = [], saved) =>
      todos.map((todo) => (todo.pending ? saved : todo)),
  },
});
// mutate(newTodo) updates ['todos'] instantly;
// on error the cache rolls back to the pre-mutation snapshot.

Or manual, via the onMutate/onError lifecycle:

const { mutate } = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries(['todos']);
    const previous = queryClient.getQueryData(['todos']);
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
    return { previous };
  },
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previous);
  },
});

Live Queries (WebSocket / SSE)

const sseSource = {
  subscribe: (listener) => {
    const events = new EventSource(`/api/channels/${channelId}/stream`);
    events.onmessage = (e) => listener(JSON.parse(e.data));
    return () => events.close();
  },
};

const { data: messages } = useSubscription({
  queryKey: ['messages', channelId],
  queryFn: () => fetchMessages(channelId),
  source: sseSource,
  onEvent: (message, messages = []) => [...messages, message],
});

With TypeScript

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

const { data } = useQuery<User, Error>({
  queryKey: ['user', id],
  queryFn: () => fetchUser(id),
});
// data is User | undefined

Configuration

const queryClient = new QueryClient({
  defaultOptions: {
    staleTime: 0,              // Data is stale immediately
    cacheTime: 5 * 60 * 1000,  // 5 minutes
    retry: 3,
    retryDelay: 1000,
    refetchOnMount: true,
    refetchOnWindowFocus: true,
    refetchOnReconnect: true,
  },
  devtools: {
    enabled: process.env.NODE_ENV === 'development',
    maxLogs: 100,
  },
});

Browser Support

| Browser | Version | |---------|---------| | Chrome | 90+ | | Firefox | 90+ | | Safari | 14+ | | Edge | 90+ |


Contributing

We welcome contributions! See CONTRIBUTING.md.

License

MIT © ForgeDevStack