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

graphql-infinite-query

v0.4.0

Published

React hook and select component for GraphQL infinite scroll / pagination with Apollo Client

Readme

graphql-infinite-query

A React hook and drop-in select component for GraphQL infinite scroll / pagination with Apollo Client.

  • Zero boilerplate — one hook call handles fetching, merging, and searching
  • Works with any response shape — extract items and pagination with simple getter functions
  • Built-in debounced search — with an immediate variant for programmatic use
  • Smart cache integration — uses Apollo fetchMore + updateQuery so the cache stays consistent
  • Custom merge logic — deduplicate or reorder incoming pages however you like
  • Full TypeScript support — all generics flow from query → item type automatically

Installation

npm install graphql-infinite-query

Peer dependencies

npm install react react-dom @apollo/client graphql

Exports

| Export | Type | Description | |---|---|---| | useInfiniteLoadQuery | Hook | Core hook — fetches pages, merges results, handles search | | InfiniteSelect | Component | Ready-made searchable dropdown with infinite scroll | | checkHasBottomReached | Utility | Detects scroll-to-bottom inside an onScroll handler | | Pagination | Type | { pageNumber, pageSize } | | PaginationResponse | Type | { pageNumber, pageSize, total, totalPage } |


useInfiniteLoadQuery

The hook is agnostic about your API shape. You tell it how to extract items and pagination via two getter functions, and it handles everything else.

Expected GraphQL response shape

{
  anyQueryName {
    pagination { pageNumber pageSize total totalPage }
    items { ... }
  }
}

Basic usage

import { useInfiniteLoadQuery, checkHasBottomReached } from 'graphql-infinite-query';
import { gql } from '@apollo/client';

const GET_USERS = gql`
  query GetUsers($pagination: PaginationInput, $filter: String) {
    getUsers(pagination: $pagination, filter: $filter) {
      pagination { pageNumber pageSize total totalPage }
      items { id name email }
    }
  }
`;

function UserList({ client }) {
  const {
    data,           // TData[] — accumulated items across all loaded pages
    loading,        // true while any request is in flight
    isFetchingMore, // true only during a next-page fetch (great for spinners)
    hasNextPage,
    loadNextPage,
    onSearch,       // debounced — wire directly to an input onChange
    reset,
    error,
  } = useInfiniteLoadQuery({
    query: GET_USERS,
    clientInstance: client,
    getItems: (data) => data.getUsers.items,
    getPagination: (data) => data.getUsers.pagination,
    variables: (pagination, search) => ({
      pagination,
      filter: search,
    }),
  });

  return (
    <div onScroll={e => { if (checkHasBottomReached(e)) loadNextPage(); }}>
      <input onChange={e => onSearch(e.target.value)} placeholder="Search..." />

      {data.map(user => <div key={user.id}>{user.name}</div>)}

      {isFetchingMore && <span>Loading more…</span>}
      {!hasNextPage && <span>All loaded</span>}
      {error && <span>Error: {error.message}</span>}
    </div>
  );
}

Custom merge — deduplication example

By default incoming items are appended. Pass mergeItems to override:

useInfiniteLoadQuery({
  // ...
  mergeItems: (existing, incoming) => {
    const seen = new Set(existing.map(i => i.id));
    return [...existing, ...incoming.filter(i => !seen.has(i.id))];
  },
});

Programmatic search (no debounce)

const { onSearchImmediate } = useInfiniteLoadQuery({ ... });

// Call immediately — useful for controlled inputs or external triggers
onSearchImmediate('John');

Props

| Prop | Type | Default | Required | Description | |---|---|---|---|---| | query | DocumentNode | — | Yes | GraphQL query document | | clientInstance | ApolloClient | — | Yes | Apollo Client instance | | getItems | (data: TQuery) => TData[] | — | Yes | Extract the item array from the query response | | getPagination | (data: TQuery) => PaginationResponse | — | Yes | Extract pagination metadata from the query response | | variables | (pagination, search?) => TVariables | { pagination, filter: search } | No | Factory that builds query variables per page/search | | mergeItems | (existing, incoming) => TData[] | append | No | Custom strategy for merging pages (e.g. deduplication) | | skip | boolean | false | No | Skip query execution | | fetchPolicy | string | 'cache-first' | No | Apollo fetch policy | | defaultPagination | Pagination | { pageNumber: 1, pageSize: 10 } | No | Initial pagination — also the reset target | | debounceTime | number | 500 | No | Debounce delay in ms for onSearch | | context | DefaultContext | — | No | Apollo context forwarded to every request (e.g. auth headers) |

Returns

| Value | Type | Description | |---|---|---| | data | TData[] | Accumulated flat list of items across all pages loaded so far | | loading | boolean | true while the initial query or a fetchMore is in flight | | isFetchingMore | boolean | true only during a next-page fetch — ideal for a bottom spinner | | hasNextPage | boolean | true when there is at least one more page to fetch | | loadNextPage | () => void | Fetches and appends the next page; no-op when loading or no next page | | onSearch | DebouncedFunc<(value: string) => void> | Debounced search — resets to page 1 after debounceTime ms | | onSearchImmediate | (value: string) => void | Non-debounced version of onSearch | | searchValue | string \| null | Currently active search string (null before the first search) | | pagination | PaginationResponse | Pagination metadata from the most recent successful response | | reset | () => void | Cancels pending debounce, clears search, and refetches from page 1 | | error | ApolloError \| undefined | Error from the most recent failed request |


InfiniteSelect

A ready-made searchable dropdown that wires useInfiniteLoadQuery to an input and a scrollable list. No extra state needed on your end.

Type parameters

InfiniteSelect<ItemDataType, QueryType>

| Type parameter | Description | |---|---| | ItemDataType | The shape of each item in the list — what getItems returns per element. This is the type you receive in getKey, renderItem, and onChange. | | QueryType | The shape of the full API response — what Apollo returns for the entire query. This is what getItems and getPagination receive as their argument. |

Example types for a getUsers query:

// The full API response
type UserItemReResponse = {
  getUsers: {
    pagination: { pageNumber: number; pageSize: number; total: number; totalPage: number };
    items: GetUsersQueryResponse[];
  };
};


```tsx
import { InfiniteSelect } from 'graphql-infinite-query';

<InfiniteSelect<UserItemReResponse, GetUsersQueryResponse>
  query={GET_USERS}
  clientInstance={client}
  getItems={(data) => data.getUsers.items}
  getPagination={(data) => data.getUsers.pagination}
  variables={(pagination, search) => ({ pagination, filter: search })}
  getKey={(user) => user.id}
  renderItem={(user) => <span>{user.name}</span>}
  onChange={(user) => console.log('selected', user)}
  placeholder="Search users…"
/>

Props

| Prop | Type | Default | Required | Description | |---|---|---|---|---| | query | DocumentNode | — | Yes | GraphQL query document | | clientInstance | ApolloClient | — | Yes | Apollo Client instance | | getItems | (data: TQuery) => TData[] | — | Yes | Extract the item array from the query response | | getPagination | (data: TQuery) => PaginationResponse | — | Yes | Extract pagination metadata from the query response | | getKey | (item: TData) => string \| number | — | Yes | Stable React key for each list item | | renderItem | (item: TData) => ReactNode | — | Yes | Render a single list row | | variables | (pagination, search?) => vars | — | No | Variables factory | | onChange | (item: TData) => void | — | No | Called when the user selects an item | | disabled | boolean | false | No | Disables the input and dropdown | | placeholder | string | — | No | Input placeholder text | | emptyText | string | 'No Data' | No | Message shown when the list is empty | | bottomOffset | number | 30 | No | Scroll distance in px from the bottom that triggers the next page | | skip | boolean | false | No | Skip query execution | | fetchPolicy | string | 'cache-first' | No | Apollo fetch policy | | defaultPagination | Pagination | { pageNumber: 1, pageSize: 10 } | No | Initial pagination | | debounceTime | number | 500 | No | Search debounce delay in ms |

Note: InfiniteSelect uses Tailwind CSS utility classes. Make sure Tailwind is configured in your project.


checkHasBottomReached

A small utility for building your own infinite-scroll containers. Returns true when the scrollable element is within bottomOffset pixels of its bottom.

import { checkHasBottomReached } from 'graphql-infinite-query';

<div
  style={{ height: 400, overflowY: 'auto' }}
  onScroll={e => {
    if (checkHasBottomReached(e, 50)) {
      loadNextPage();
    }
  }}
>
  {items.map(item => <Row key={item.id} item={item} />)}
</div>

| Parameter | Type | Default | Description | |---|---|---|---| | event | UIEvent<HTMLDivElement> | — | React scroll event from the scrollable container | | bottomOffset | number | 30 | Pixels from the true bottom that still count as "reached" — increase for earlier pre-loading |


License

MIT