react-select-async-paginate
React Select wrapper that adds pagination-on-scroll to async, remote-loaded option lists
Repository Health
Technical Analysis
react-select-async-paginate wraps react-select’s Select, Async, and Creatable variants with hooks and a higher-order component that load menu options page by page as the user scrolls, types, or opens the menu. Instead of hand-rolling debounce timers, per-search caches, and “has more” bookkeeping around react-select’s own Async loader, consumers implement a single loadOptions(inputValue, loadedOptions, additional) function and the library manages request sequencing, caching per search string, and result concatenation.
The package is deliberately small in scope: it exposes a drop-in AsyncPaginate component for the common case, plus useAsyncPaginate/useAsyncPaginateBase hooks and a withAsyncPaginate HOC for wrapping custom or third-party Select components (e.g. Creatable) with the same pagination behavior. Options are cached by the current search string so switching between an empty search and a typed query doesn’t refetch already-loaded pages, and helpers like reduceGroupedOptions support paginating grouped option lists.
What You Get
AsyncPaginate— a drop-in react-select replacement that paginates options as the menu scrollswithAsyncPaginateHOC to add the same pagination behavior toCreatableor any other custom Select componentuseAsyncPaginate/useAsyncPaginateBasehooks for building fully custom select UIs on top of the same loading/caching logic- Per-search-string options cache that avoids re-fetching pages already loaded for a given query
- Configurable debounce (
debounceTimeout), retry-after-error timeout (reloadOnErrorTimeout), and cache invalidation (clearCacheOnSearchChange,clearCacheOnMenuClose) reduceGroupedOptionshelper for paginating grouped (labelled) option lists
Common Use Cases
- Populating a select input from a paginated REST or GraphQL endpoint (offset- or page-based) without loading the whole dataset upfront
- Type-ahead search-as-you-type against a remote API with built-in debouncing
- Large reference-data pickers (users, organizations, products) where the full list is too big to load at once
- Adding “load more on scroll” behavior to a
Creatableselect that also lets users add new options - Building a fully custom select UI that still needs react-select-async-paginate’s caching/pagination semantics via its hooks
Under The Hood
Architecture
The library is organized as small, individually unit-tested pure functions (defaultReduceOptions, defaultShouldLoadMore, getInitialCache, getInitialOptionsCache, validateResponse) composed by two layered hooks: useAsyncPaginateBase (in src/useAsyncPaginateBase.ts) owns a ref-backed per-search-string options cache and drives loading via requestOptions (src/requestOptions.ts), while useAsyncPaginate layers react-select’s inputValue/menuIsOpen state on top. withAsyncPaginate (src/withAsyncPaginate.tsx) is a generic HOC that injects the hook’s resulting props into any react-select component, which is how the exported AsyncPaginate (withAsyncPaginate(Select)) is built. This split means the core scroll/cache/debounce logic has exactly one implementation shared by the default component, the Creatable wrapping path, and fully custom consumers using the hooks directly — changing requestOptions.ts changes behavior everywhere at once.
Tech Stack
Written in TypeScript against react-select ^5 as a peer dependency, with a handful of small focused dependencies: @vtaits/use-lazy-ref and use-latest for ref-stable callbacks across renders, use-is-mounted-ref to guard against post-unmount state updates, krustykrab for a Result-style wrapper around the async loadOptions call, and sleep-promise for debounce delays. The package builds with tsup to dual ESM/CJS output plus .d.ts typings, and the repo is a Bun-managed monorepo (this package plus react-select-fetch) using Biome for linting/formatting.
Code Quality
Every pure helper module has a matching *.test.ts file run under Vitest (including a browser-mode config for requestOptions/index integration tests), giving comprehensive coverage of the caching, reduction, and validation logic specifically, though there is comparatively less test coverage of the top-level hooks’ render-cycle behavior itself. Error handling is explicit via krustykrab’s Result type (isErr()/unwrap()) rather than bare try/catch, and tsc --noEmit plus Biome enforce typing and style. No GitHub Actions CI configuration is present in the current repo snapshot (a legacy .travis.yml remains from an earlier setup), though a Codecov badge suggests CI runs are wired up elsewhere.
API Design
The public surface is intentionally minimal — a consumer only has to supply loadOptions, and everything else (debounce, hasMore tracking, per-search caching) has sensible defaults. The same underlying logic is exposed at three levels of control — the ready-made AsyncPaginate component, the withAsyncPaginate HOC for wrapping other Select variants, and the raw useAsyncPaginate/useAsyncPaginateBase hooks for fully custom UIs — so consumers can opt into more control without reimplementing pagination semantics themselves.