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

@nastmz/light-query

v1.0.3

Published

Light-query is a minimal, from-scratch React data-fetching library inspired by TanStack Query. It provides easy-to-use hooks for queries, mutations, and infinite scrolling with configurable caching, stale-time, retry logic, and optional React Suspense sup

Readme

🚀 light-query

A lightweight data-fetching library for React inspired by TanStack Query

React 18+ TypeScript 5.0+ MIT License

Performant • Lightweight • Easy to use • Fully typed


🎯 Overview

light-query is a modern data-fetching library that provides powerful async state management for React applications. Built with TypeScript, it offers intelligent caching, concurrent request prevention, and automatic background updates while maintaining a simple and intuitive API.

Note: This library is designed for exploration and learning purposes. While fully functional and well-tested, it is not intended as a commercial solution.

✨ Key Features

A comprehensive data-fetching solution with advanced capabilities:

| Feature | Description | | --------------------------- | ----------------------------------------------------------- | | 🔄 Queries & Mutations | Declarative async state management with intuitive API | | 🚀 Infinite Queries | Basic pagination and infinite loading support | | 💾 Smart Caching | Intelligent cache system with automatic invalidation | | ⚡ Performance | Concurrent request prevention and batch notification system | | 🎯 TypeScript | Full TypeScript support with advanced type safety | | 🧪 Testing | Basic testing utilities and mock support | | 🔧 Configuration | Flexible, global configuration system | | 📊 State Tracking | Reactive state monitoring and global query tracking | | 🚫 Request Cancellation | Automatic request cancellation with AbortController | | ⚛️ React Suspense | Native React Suspense integration |

🎨 Technical Highlights

Advanced implementation features:

  • 📊 State Management - Sophisticated async state patterns and lifecycle management
  • 🔄 Data Synchronization - Automatic UI-server synchronization with cache invalidation
  • 🏗️ Architecture Design - Modular, extensible architecture with clear separation of concerns
  • 🛠️ TypeScript - Advanced type system usage with generics and conditional types
  • 🧪 Testing Patterns - Basic testing utilities for async React components
  • 🔍 Performance Optimization - Memory-efficient rendering with caching and concurrent request prevention

📦 Installation & Setup

npm i @nastmz/light-query

🚀 Quick Start

Step 1: Setup the Provider

import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@nastmz/light-query";
import App from "./App";

// Create client with custom configuration
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
      retry: 3,
      refetchInterval: 0,
      suspense: false,
    },
  },
  maxCacheSize: 50,
  logger: {
    error: (message, meta) => console.error(message, meta),
    warn: (message, meta) => console.warn(message, meta),
  }, // Optional: logging system
});

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);

Step 2: Your First Query

import React from "react";
import { useQuery } from "@nastmz/light-query";

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

function UserProfile({ userId }: { userId: number }) {
  const { data, status, error, refetch } = useQuery<User>({
    queryKey: ["user", userId],
    queryFn: async ({ signal }) => {
      const response = await fetch(`/api/users/${userId}`, { signal });
      if (!response.ok) throw new Error("Failed to fetch user");
      return response.json();
    },
  });

  if (status === "loading") return <div>Loading...</div>;
  if (error)
    return (
      <div>Error: {error instanceof Error ? error.message : String(error)}</div>
    );

  return (
    <div>
      <h1>{data?.name}</h1>
      <p>{data?.email}</p>
      <button onClick={() => refetch()}>Refresh</button>
    </div>
  );
}

Step 3: Your First Mutation

import React, { useState } from "react";
import { useMutation, useQueryClient } from "@nastmz/light-query";

function CreateUser() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const queryClient = useQueryClient();

  const createUser = useMutation({
    mutationFn: async (newUser: { name: string; email: string }) => {
      const response = await fetch("/api/users", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(newUser),
      });
      return response.json();
    },
    onSuccess: async () => {
      await queryClient.invalidateQueries(["users"]);
      setName("");
      setEmail("");
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    createUser.mutate({ name, email });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
        required
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <button type="submit" disabled={createUser.status === "loading"}>
        {createUser.status === "loading" ? "Creating..." : "Create User"}
      </button>
    </form>
  );
}

🔍 Core Concepts

Caching and Invalidation

light-query implements an intelligent caching system that:

  • Concurrent request prevention: Prevents multiple simultaneous requests for the same query
  • Automatic invalidation: Refetches when data changes
  • Automatic cleanup: Removes stale data based on cacheTime
  • Sharing: Multiple components can share the same query
// Invalidate specific queries
await queryClient.invalidateQueries(["users"]);

// Invalidate queries starting with 'users'
await queryClient.invalidateQueries(["users"]);

// Update cache directly
queryClient.setQueryData(["user", 1], newUserData);

Query States

const { status, data, error, updatedAt } = useQuery({
  queryKey: ["data"],
  queryFn: fetchData,
});

// status can be: 'idle', 'loading', 'error', 'success'
// You can derive loading states from status:
// const isLoading = status === 'loading'
// const isSuccess = status === 'success'
// const isError = status === 'error'

Query Options

useQuery({
  queryKey: ["posts", { page: 1 }],
  queryFn: fetchPosts,

  // Cache configuration
  staleTime: 5 * 60 * 1000, // Data is "fresh" for 5 minutes
  cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes

  // Behavior
  retry: 3, // Retry on error
  retryDelay: 1000, // Delay between retries

  // Automatic refetch
  refetchInterval: 30 * 1000, // Every 30 seconds

  // Suspense
  suspense: false, // Enable React Suspense
});

📖 API Reference

Hooks

useQuery<T>(options: QueryOptions<T>)

Main hook for data fetching with automatic cache and state management.

Parameters:

  • queryKey - Unique key to identify the query
  • queryFn - Function that returns a Promise with the data
  • staleTime - Time in ms before data is considered stale
  • cacheTime - Time in ms data stays in cache
  • retry - Number of retries on error
  • retryDelay - Delay between retries in ms
  • refetchInterval - Automatic refetch interval in ms
  • suspense - Enable React Suspense support

Returns:

  • data - The query data
  • error - Error if the query failed
  • status - Current state (idle, loading, success, error)
  • updatedAt - Timestamp of last successful fetch or error
  • refetch - Function for manual refetch

useMutation<TData, TVariables>(options: MutationOptions<TData, TVariables>)

Hook for mutations (POST, PUT, DELETE, etc.) with success and error callbacks.

const mutation = useMutation({
  mutationFn: (variables) => createUser(variables),
  onSuccess: (data) => {
    // Success logic
  },
  onError: (error) => {
    // Error handling
  },
});

// Use the mutation
mutation.mutate(userData);

useInfiniteQuery<T>(options: InfiniteQueryOptions<T>)

Hook for paginated queries with infinite loading.

const result = useInfiniteQuery({
  queryKey: ["posts"],
  queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
  getNextPageParam: (lastPage, pages) => lastPage.nextPage,
});

useQueryClient()

Hook to access the QueryClient and its methods.

const queryClient = useQueryClient();

// Invalidate queries
await queryClient.invalidateQueries(["users"]);

// Update cache
queryClient.setQueryData(["user", 1], newUserData);

useIsFetching()

Hook that returns the number of queries currently fetching.

const isFetching = useIsFetching();
// Returns: number

useIsMutating()

Hook that returns the number of mutations currently executing.

const mutatingCount = useIsMutating();
// Returns: number

QueryClient

Main class for managing global query and mutation state.

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,
      cacheTime: 10 * 60 * 1000,
      retry: 3,
      retryDelay: 1000,
      refetchInterval: 0,
      suspense: false,
    },
  },
  maxCacheSize: 50,
  logger: {
    error: (message, meta) => console.error(message, meta),
    warn: (message, meta) => console.warn(message, meta),
  },
});

Main Methods

  • invalidateQueries(queryKey) - Invalidate specific queries (async)
  • cancelQueries(queryKey) - Cancel running queries
  • getQueryData(queryKey) - Get data from cache
  • setQueryData(queryKey, data) - Update cache data
  • getQueries(queryKey) - Get queries matching a pattern
  • clear() - Clear all cache
  • getActiveMutationCount() - Get number of active mutations

TypeScript Types

interface QueryOptions<T> {
  queryKey: QueryKey;
  queryFn: (context?: { signal?: AbortSignal }) => Promise<T>;
  staleTime?: number;
  cacheTime?: number;
  retry?: number;
  retryDelay?: number;
  refetchInterval?: number;
  suspense?: boolean;
}

interface MutationOptions<TData, TVariables> {
  mutationFn: (variables: TVariables) => Promise<TData>;
  onSuccess?: (data: TData) => void;
  onError?: (error: Error) => void;
}

interface QueryResult<T> {
  data: T | undefined;
  error: unknown;
  status: "idle" | "loading" | "success" | "error";
  updatedAt: number;
  refetch: () => Promise<void>;
}

🎯 Advanced Patterns

Query Dependencies

function UserProfile({ userId }: { userId: number }) {
  // Main query to get user
  const userQuery = useQuery({
    queryKey: ["user", userId],
    queryFn: () => fetchUser(userId),
  });

  // Dependent query that only runs if user exists
  const postsQuery = useQuery({
    queryKey: ["posts", userId],
    queryFn: () => fetchUserPosts(userId),
    // Note: This example shows conditional logic, but 'enabled' is not implemented
    // You would need to handle this in your component logic
  });

  return (
    <div>
      {userQuery.data && (
        <div>
          <h1>{userQuery.data.name}</h1>
          {postsQuery.data && (
            <ul>
              {postsQuery.data.map((post) => (
                <li key={post.id}>{post.title}</li>
              ))}
            </ul>
          )}
        </div>
      )}
    </div>
  );
}

Mutation Handling

function UpdateTodo({ todo }: { todo: Todo }) {
  const queryClient = useQueryClient();

```tsx
function UpdateTodo({ todo }: { todo: Todo }) {
  const queryClient = useQueryClient();

  const updateTodo = useMutation({
    mutationFn: (updatedTodo: Partial<Todo>) =>
      updateTodoApi(todo.id, updatedTodo),

    // Note: onMutate and onSettled are not implemented in this library
    // This is a conceptual example showing how optimistic updates would work
    onSuccess: async (data) => {
      // Refetch to sync with server
      await queryClient.invalidateQueries(["todos"]);
    },

    onError: (error) => {
      // Handle error
      console.error("Failed to update todo:", error);
    },
  });

  const handleToggle = () => {
    // For optimistic updates, you would handle them manually:
    // 1. Update local state optimistically
    // 2. Perform mutation
    // 3. Revert on error or sync on success
    updateTodo.mutate({ completed: !todo.completed });
  };

  return (
    <div>
      <input type="checkbox" checked={todo.completed} onChange={handleToggle} />
      <span
        style={{
          textDecoration: todo.completed ? "line-through" : "none",
        }}
      >
        {todo.title}
      </span>
    </div>
  );
}

Parallel Queries

function Dashboard() {
  // Multiple queries running in parallel
  const userQuery = useQuery({
    queryKey: ["user"],
    queryFn: fetchCurrentUser,
  });

  const statsQuery = useQuery({
    queryKey: ["stats"],
    queryFn: fetchStats,
  });

  const notificationsQuery = useQuery({
    queryKey: ["notifications"],
    queryFn: fetchNotifications,
  });

  // Use useIsFetching to show global state
  const isFetching = useIsFetching();

  return (
    <div>
      {isFetching > 0 && (
        <div>🔄 Loading data... ({isFetching} active queries)</div>
      )}

      <UserSection
        user={userQuery.data}
        loading={userQuery.status === "loading"}
      />
      <StatsSection
        stats={statsQuery.data}
        loading={statsQuery.status === "loading"}
      />
      <NotificationSection
        notifications={notificationsQuery.data}
        loading={notificationsQuery.status === "loading"}
      />
    </div>
  );
}

Search with Debounce

function SearchResults() {
  const [searchTerm, setSearchTerm] = useState("");
  const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");

  // Debounce search term
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedSearchTerm(searchTerm);
    }, 300);

    return () => clearTimeout(timer);
  }, [searchTerm]);

  const searchQuery = useQuery({
    queryKey: ["search", debouncedSearchTerm],
    queryFn: () => searchApi(debouncedSearchTerm),
    // Note: You would need to handle conditional fetching in your component
    staleTime: 2 * 60 * 1000, // Cache for 2 minutes
  });

  // Only render results if we have a search term
  if (debouncedSearchTerm.length <= 2) {
    return (
      <div>
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="Search..."
        />
        <div>Type at least 3 characters to search...</div>
      </div>
    );
  }

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />

      {searchQuery.status === "loading" && <div>Searching...</div>}
      {searchQuery.error && (
        <div>
          Error:{" "}
          {searchQuery.error instanceof Error
            ? searchQuery.error.message
            : String(searchQuery.error)}
        </div>
      )}

      {searchQuery.data && (
        <div>
          {searchQuery.data.map((result) => (
            <div key={result.id}>{result.title}</div>
          ))}
        </div>
      )}
    </div>
  );
}

🛠️ Advanced Features

Request Cancellation

light-query includes automatic request cancellation support using AbortController:

const { data, refetch } = useQuery({
  queryKey: ["data"],
  queryFn: async ({ signal }) => {
    const response = await fetch("/api/data", {
      signal, // AbortController signal
    });
    return response.json();
  },
});

// Queries are automatically cancelled when:
// - Component unmounts
// - queryKey changes
// - New query with same key is executed

React Suspense

function TodosWithSuspense() {
  const { data } = useQuery({
    queryKey: ["todos"],
    queryFn: fetchTodos,
    suspense: true, // Enable Suspense
  });

  // No need to handle loading state
  // Suspense handles it automatically
  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

// In your App
function App() {
  return (
    <Suspense fallback={<div>Loading todos...</div>}>
      <TodosWithSuspense />
    </Suspense>
  );
}

Logging System

import { Logger, LogLevel } from "@nastmz/light-query";

// Create a logger instance
const logger = new Logger(LogLevel.Info);

const queryClient = new QueryClient({
  logger: {
    error: (message, meta) => logger.error(message, meta),
    warn: (message, meta) => logger.warn(message, meta),
  },
});

🧪 Testing

light-query includes comprehensive testing utilities:

Testing Utilities

import {
  createMockQueryClient,
  waitForQuery,
} from "@nastmz/light-query/test-utils";
import { render, screen, waitFor } from "@testing-library/react";
import { QueryClientProvider } from "@nastmz/light-query";

describe("TodoList", () => {
  it("should render todos after loading", async () => {
    const mockClient = createMockQueryClient();

    render(
      <QueryClientProvider client={mockClient}>
        <TodoList />
      </QueryClientProvider>
    );

    // Wait for query to complete
    await waitForQuery(mockClient, ["todos"]);

    expect(screen.getByText("Todo 1")).toBeInTheDocument();
    expect(screen.getByText("Todo 2")).toBeInTheDocument();
  });

  it("should handle error state", async () => {
    const mockClient = createMockQueryClient({
      defaultOptions: {
        queries: {
          retry: false, // Disable retry for tests
        },
      },
    });

    // Simulate query error
    mockClient.setQueryData(["todos"], () => {
      throw new Error("Network error");
    });

    render(
      <QueryClientProvider client={mockClient}>
        <TodoList />
      </QueryClientProvider>
    );

    await waitFor(() => {
      expect(screen.getByText(/error/i)).toBeInTheDocument();
    });
  });
});

📊 Performance

light-query is optimized for performance:

Built-in Optimizations

  • Concurrent Request Prevention: Prevents multiple simultaneous requests for the same query
  • Batch Notifications: Multiple updates are batched to prevent unnecessary re-renders
  • Automatic Cancellation: Stale requests are cancelled automatically
  • Smart Cleanup: Cache is cleaned automatically based on cacheTime
  • Lazy Loading: Queries only execute when needed

Best Practices

  1. Use descriptive queryKeys:

    // ❌ Bad
    useQuery({ queryKey: ["data"], queryFn: fetchData });
    
    // ✅ Good
    useQuery({ queryKey: ["posts", "user", userId], queryFn: fetchUserPosts });
  2. Configure staleTime appropriately:

    // For data that changes infrequently
    useQuery({
      queryKey: ["settings"],
      queryFn: fetchSettings,
      staleTime: 10 * 60 * 1000, // 10 minutes
    });
    
    // For data that changes frequently
    useQuery({
      queryKey: ["notifications"],
      queryFn: fetchNotifications,
      staleTime: 30 * 1000, // 30 seconds
    });

🛠️ Development

Environment Setup

# Clone the repository
git clone https://github.com/NastMz/light-query.git
cd light-query

# Install dependencies
npm install

# Run tests
npm test

# Run tests in watch mode
npm test -- --watch

# Build for production
npm run build

# Lint code
npm run lint

# Format code
npm run format

Available Scripts

  • npm test - Run all tests with vitest
  • npm run test:watch - Run tests in watch mode
  • npm run test:coverage - Run tests with coverage
  • npm run build - Production build with rollup
  • npm run lint - Lint with ts-standard
  • npm run format - Format code with ts-standard

Project Structure

light-query/
├── src/
│   ├── hooks/           # Main hooks
│   ├── core/            # Core logic
│   ├── react/           # React components
│   ├── types/           # TypeScript types
│   ├── utils/           # Utilities
│   ├── mocks/           # MSW mocks for testing
│   ├── test-utils/      # Testing utilities
│   └── index.ts         # Main exports
├── __tests__/           # Tests
├── examples/            # Usage examples
├── package.json
├── tsconfig.json
├── rollup.config.mjs
└── vitest.config.ts

🎓 Technical Implementation

This library demonstrates comprehensive implementation of:

Core Technologies

  • React Hooks: Advanced custom hooks, useEffect patterns, useRef, useCallback
  • State Management: Complex state patterns, state machines, and reactive systems
  • Async Operations: Promise handling, error boundaries, and automatic cancellation
  • TypeScript: Generics, conditional types, utility types, and advanced type safety
  • Testing: React Testing Library, async testing patterns, and comprehensive mocking

Advanced Implementation Details

  • Observer Pattern: Reactive subscription system with efficient change detection
  • Cache Management: Simple cache with TTL, intelligent invalidation strategies
  • Performance Optimization: Concurrent request prevention, batching, and automatic cleanup
  • Error Handling: Custom error types with recovery strategies and user-friendly messages
  • Configuration Systems: Flexible, typed configuration with sensible defaults

Architecture Patterns

  • Modular Design: Clean separation of concerns with clear interfaces
  • API Design: Intuitive, consistent API with TypeScript-first approach
  • Plugin Architecture: Extensible system with configurable components
  • Documentation: Comprehensive API documentation with practical examples

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

👨‍💻 Author

Kevin Martinez - @NastMz


⭐ If you find this project useful, consider giving it a star on GitHub! ⭐