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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@untools/react-query-hooks

v0.2.3

Published

TypeScript-first React hooks for data fetching, pagination, and query management

Downloads

184

Readme

@untools/react-query-hooks

npm version license TypeScript

A lightweight, TypeScript-first library of React hooks for data fetching, pagination, and query management. Built for modern React applications that need flexible data fetching solutions.

Features

  • 🔄 Simple data fetching with useQuery
  • 📄 Pagination support with usePaginatedQuery
  • ⏱️ Debouncing for search and filter inputs
  • 🔍 Sorting and filtering capabilities
  • 🧩 TypeScript support with full type safety
  • 🎯 Customizable types for pagination, sorting, and metadata
  • 🪶 Lightweight with zero dependencies
  • 🧪 Well tested and reliable

Installation

# npm
npm install @untools/react-query-hooks

# yarn
yarn add @untools/react-query-hooks

# pnpm
pnpm add @untools/react-query-hooks

Usage

Basic Query

The useQuery hook provides a simple way to fetch data:

import { useQuery } from '@untools/react-query-hooks';

function UserProfile({ userId }) {
  const { data, isLoading, error, refresh } = useQuery({
    service: () => fetch(`/api/users/${userId}`).then(res => res.json()),
    immediate: true
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

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

Paginated Query

The usePaginatedQuery hook handles pagination, sorting, and filtering:

import { usePaginatedQuery } from '@untools/react-query-hooks';

function UserList() {
  const {
    data,
    isLoading,
    error,
    setPagination,
    setFilters,
    setSort,
    refresh
  } = usePaginatedQuery({
    service: {
      getData: (options) => fetch(`/api/users?page=${options.pagination.page}&limit=${options.pagination.limit}&search=${options.filters?.search || ''}`).then(res => res.json())
    },
    initialPagination: { page: 1, limit: 10 },
    initialFilters: { search: '' },
    debounceTime: 300
  });

  if (isLoading && !data.data.length) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <input
        type="text"
        placeholder="Search users..."
        onChange={(e) => setFilters({ search: e.target.value })}
      />
      
      <table>
        <thead>
          <tr>
            <th onClick={() => setSort({ field: 'name', direction: 'asc' })}>Name</th>
            <th onClick={() => setSort({ field: 'email', direction: 'asc' })}>Email</th>
          </tr>
        </thead>
        <tbody>
          {data.data.map(user => (
            <tr key={user.id}>
              <td>{user.name}</td>
              <td>{user.email}</td>
            </tr>
          ))}
        </tbody>
      </table>
      
      <div className="pagination">
        <button 
          disabled={!data.meta.hasPrevPage}
          onClick={() => setPagination({ page: data.meta.page - 1, limit: data.meta.limit })}
        >
          Previous
        </button>
        <span>Page {data.meta.page} of {data.meta.pages}</span>
        <button 
          disabled={!data.meta.hasNextPage}
          onClick={() => setPagination({ page: data.meta.page + 1, limit: data.meta.limit })}
        >
          Next
        </button>
      </div>
      
      <button onClick={refresh}>Refresh</button>
    </div>
  );
}

Custom Types for Different APIs

One of the key features of this library is the ability to customize types for pagination, sorting, and metadata to match your API's response structure:

import { usePaginatedQuery } from '@untools/react-query-hooks';

// Define custom types to match your API
interface CustomMeta {
  page?: number;
  limit?: number;
  total?: number;
  totalPages?: number;
  hasNextPage?: boolean;
  hasPreviousPage?: boolean; // Your API uses this instead of hasPrevPage
}

interface CustomPagination {
  pageNumber?: number;
  pageSize?: number;
  offset?: number;
}

interface CustomSort {
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
}

interface UserFilters {
  search?: string;
  status?: 'active' | 'inactive';
}

interface User {
  id: string;
  name: string;
  email: string;
  status: 'active' | 'inactive';
}

function CustomUserList() {
  const {
    data,
    isLoading,
    error,
    setPagination,
    setFilters,
    setSort,
  } = usePaginatedQuery<UserFilters, User, CustomPagination, CustomSort, CustomMeta>({
    service: {
      getData: async (options) => {
        const response = await fetch('/api/users', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(options),
        });
        return response.json();
      },
    },
    initialPagination: { pageNumber: 1, pageSize: 20 },
    initialSort: { sortBy: 'name', sortOrder: 'asc' },
    initialFilters: { status: 'active' },
  });

  return (
    <div>
      {/* Your component JSX */}
      <button 
        disabled={!data.meta?.hasPreviousPage}
        onClick={() => setPagination(prev => ({ 
          ...prev, 
          pageNumber: (prev.pageNumber || 1) - 1 
        }))}
      >
        Previous
      </button>
      
      <button 
        disabled={!data.meta?.hasNextPage}
        onClick={() => setPagination(prev => ({ 
          ...prev, 
          pageNumber: (prev.pageNumber || 1) + 1 
        }))}
      >
        Next
      </button>
    </div>
  );
}

Debounced Input

You can also use the useDebounce hook directly:

import { useDebounce } from '@untools/react-query-hooks';
import { useState } from 'react';

function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 300);
  
  // debouncedSearchTerm will update 300ms after searchTerm stops changing
  // Use it for API calls to prevent excessive requests
  
  return (
    <input
      type="text"
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      placeholder="Search..."
    />
  );
}

Enabled/Disabled Queries

Control when queries should run using the enabled option:

import { usePaginatedQuery } from '@untools/react-query-hooks';

function ConditionalUserList({ shouldFetch }) {
  const {
    data,
    isLoading,
    error,
    refresh,
  } = usePaginatedQuery({
    service: {
      getData: (options) => fetchUsers(options)
    },
    enabled: shouldFetch, // Only fetch when this is true
    initialPagination: { page: 1, limit: 10 },
  });

  // You can still manually trigger the query even when disabled
  const handleManualRefresh = () => {
    refresh();
  };

  return (
    <div>
      {shouldFetch ? (
        <>
          {isLoading && <div>Loading...</div>}
          {error && <div>Error: {error.message}</div>}
          {data.data.map(user => (
            <div key={user.id}>{user.name}</div>
          ))}
        </>
      ) : (
        <button onClick={handleManualRefresh}>
          Load Users
        </button>
      )}
    </div>
  );
}

API Reference

useQuery

function useQuery<T>({
  service,
  state,
  immediate
}: QueryOptions<T>): {
  data: T | null | undefined;
  error: Error | null;
  isLoading: boolean;
  refresh: () => Promise<void>;
}

Options

  • service: Function that returns a Promise with the data
  • state?: Optional object for external state management
    • setState?: Function to update external state
    • getState?: Function to get initial state
  • immediate?: Boolean to determine if the query should run immediately (default: true)

Returns

  • data: The fetched data or null
  • error: Error object if the request failed, or null
  • isLoading: Boolean indicating if the request is in progress
  • refresh: Function to manually trigger the query

usePaginatedQuery

function usePaginatedQuery<F, D, P = DefaultPagination, S = DefaultSortInput, M = DefaultMeta>({
  service,
  initialFilters,
  initialPagination,
  initialSort,
  debounceTime,
  state,
  enabled
}: PaginatedQueryOptions<F, D, P, S, M>): {
  data: QueryResult<D, M>;
  error: Error | null;
  isLoading: boolean;
  setFilters: (filters: React.SetStateAction<F>) => void;
  setPagination: (pagination: React.SetStateAction<P>) => void;
  setSort: (sort: React.SetStateAction<S | undefined>) => void;
  refresh: () => Promise<void>;
}

Generic Type Parameters

  • F: Type for filters
  • D: Type for data items
  • P: Type for pagination (defaults to DefaultPagination)
  • S: Type for sorting (defaults to DefaultSortInput)
  • M: Type for metadata (defaults to DefaultMeta)

Options

  • service: Object containing a getData function that accepts filters, pagination, and sort options
  • initialFilters?: Initial filter values
  • initialPagination?: Initial pagination state (default: { page: 1, limit: 10 })
  • initialSort?: Initial sort configuration
  • debounceTime?: Debounce time in milliseconds for filter changes (default: 500)
  • enabled?: Boolean to control when the query should run (default: true)
  • state?: Optional object for external state management
    • setState?: Function to update external state
    • getState?: Function to get initial state

Returns

  • data: The fetched data with metadata
  • error: Error object if the request failed, or null
  • isLoading: Boolean indicating if the request is in progress
  • setFilters: Function to update filter values
  • setPagination: Function to update pagination
  • setSort: Function to update sort configuration
  • refresh: Function to manually trigger the query

useDebounce

function useDebounce<T>(value: T, delay: number): T

Parameters

  • value: The value to debounce
  • delay: Debounce delay in milliseconds

Returns

  • The debounced value

TypeScript Support

This library is built with TypeScript and provides full type safety. The hooks are generic and can be typed to match your data structures:

Basic Usage

// Define your data types
interface User {
  id: number;
  name: string;
  email: string;
}

interface UserFilters {
  search: string;
  status?: 'active' | 'inactive';
}

// Use them with the hooks
const { data } = useQuery<User>({
  service: () => fetchUser(id)
});

const { data, setFilters } = usePaginatedQuery<UserFilters, User>({
  service: {
    getData: (options) => fetchUsers(options)
  },
  initialFilters: { search: '' }
});

Custom Types for API Compatibility

// Define custom types to match your API response structure
interface CustomMeta {
  currentPage: number;
  itemsPerPage: number;
  totalItems: number;
  totalPages: number;
  hasNext: boolean;
  hasPrev: boolean;
}

interface CustomPagination {
  pageNum: number;
  size: number;
}

interface CustomSort {
  field: string;
  direction: 'ascending' | 'descending';
}

// Use with custom types
const { data } = usePaginatedQuery<
  UserFilters,
  User,
  CustomPagination,
  CustomSort,
  CustomMeta
>({
  service: {
    getData: (options) => fetchUsers(options)
  },
  initialPagination: { pageNum: 1, size: 20 },
  initialSort: { field: 'name', direction: 'ascending' }
});

// Now data.meta will have your custom structure
const canGoNext = data.meta?.hasNext;
const canGoPrev = data.meta?.hasPrev;

Default Types

If you don't specify custom types, the library uses these defaults:

// Default Pagination
interface DefaultPagination {
  page?: number | null;
  limit?: number | null;
}

// Default Sort
type DefaultSortInput = Record<string, string>;

// Default Meta
interface DefaultMeta {
  page?: number | null;
  limit?: number | null;
  pages?: number | null;
  total?: number | null;
  hasNextPage?: boolean | null;
  hasPrevPage?: boolean | null;
}

Migration Guide

From v0.1.x to v0.2.x

The library maintains full backwards compatibility. Existing code will continue to work without any changes:

// This still works exactly as before
const { data } = usePaginatedQuery<UserFilters, User>({
  service: { getData: fetchUsers },
  initialFilters: { search: '' }
});

To use the new custom types feature, simply add the additional generic parameters:

// Enhanced with custom types
const { data } = usePaginatedQuery<UserFilters, User, CustomPagination, CustomSort, CustomMeta>({
  service: { getData: fetchUsers },
  initialFilters: { search: '' }
});

License

MIT © Miracle Onyenma