@headband/react
v0.1.1
Published
React hooks and components for Headband search — an InstantSearch-compatible SDK
Maintainers
Readme
@headband/react
React hooks and components for Headband search. Drop-in building blocks for building fast, faceted search UIs -- designed as an InstantSearch-compatible alternative that works with Headband's unified search API.
Installation
npm install @headband/reactPeer dependencies: React 18+ and React DOM 18+.
Quick Start
import {
headband,
HeadbandProvider,
SearchBox,
Hits,
RefinementList,
Pagination,
Stats,
} from "@headband/react";
const client = headband("https://your-headband-instance.com", "your-api-key");
function App() {
return (
<HeadbandProvider client={client} index="movies">
<SearchBox placeholder="Search movies..." />
<Stats />
<div style={{ display: "flex", gap: "2rem" }}>
<RefinementList attribute="genres" header="Genres" />
<div>
<Hits hitComponent={MovieHit} />
<Pagination />
</div>
</div>
</HeadbandProvider>
);
}
function MovieHit({ hit }: { hit: any }) {
return (
<div>
<h3>{hit.title}</h3>
<p>{hit.overview}</p>
</div>
);
}Architecture
All state lives in <HeadbandProvider>, which holds the search query, refinements, pagination, and sort state. Child components read and write state through React hooks. Search requests are debounced (200ms) and stale responses are automatically discarded.
HeadbandProvider -- owns all search state, executes searches
|-- useSearchBox() -- query input binding
|-- useHits() -- current result hits
|-- useRefinementList() -- facet filtering
|-- usePagination() -- page navigation
|-- useStats() -- result count & timing
|-- useSortBy() -- sort order
|-- useRange() -- numeric range filters
|-- useHighlight() -- highlighted text extraction
|-- useSearch() -- full state + actions (escape hatch)Client
headband(host, apiKey)
Creates a Headband API client.
import { headband } from "@headband/react";
const client = headband("https://search.example.com", "your-api-key");| Param | Type | Description |
| -------- | -------- | --------------------------------- |
| host | string | Headband instance URL |
| apiKey | string | API key for authentication |
Returns a HeadbandClient with search() and multiSearch() methods.
Provider
<HeadbandProvider>
Wraps your search UI and manages all search state.
<HeadbandProvider
client={client}
index="products"
initialQuery="shoes"
>
{/* search components */}
</HeadbandProvider>| Prop | Type | Default | Description |
| -------------- | ----------------- | ------- | ---------------------------------------- |
| client | HeadbandClient | -- | Client returned by headband() |
| index | string | -- | Index to search |
| initialQuery | string | "" | Starting query value |
| children | ReactNode | -- | Child components |
Hooks
useSearchBox()
Binds to the search input. Returns the current query plus setters.
const { query, setQuery, clear } = useSearchBox();| Return | Type | Description |
| ---------- | -------------------------- | ---------------------------------------------- |
| query | string | Current query string |
| setQuery | (query: string) => void | Update the query (resets pagination) |
| clear | () => void | Clear the query to an empty string |
useHits<T>()
Returns the current search result hits with loading and error state.
const { hits, isLoading, error } = useHits<Movie>();| Return | Type | Description |
| ----------- | ------------- | ---------------------------------------------- |
| hits | Hit<T>[] | Array of hit objects |
| isLoading | boolean | Whether a search is in progress |
| error | Error\|null | Error from the last search, if any |
Each Hit<T> includes the original document fields, an optional _formatted object with highlighted values, and a typed __data: T accessor.
useRefinementList(props)
Provides facet filtering for a given attribute. Automatically registers the attribute so the provider includes it in search requests.
const { items, refine, canToggle, isLoading } = useRefinementList({
attribute: "brand",
limit: 10,
sortBy: "count",
});| Prop | Type | Default | Description |
| ----------- | -------------------- | --------- | ------------------------------------ |
| attribute | string | -- | Facet attribute (e.g. "genres") |
| limit | number | 20 | Max facet values to show |
| sortBy | "count" \| "alpha" | "count" | Sort order for facet values |
| Return | Type | Description |
| ----------- | ------------------------ | ------------------------------------- |
| items | RefinementListItem[] | Facet values with count & state |
| refine | (value: string) => void| Toggle a facet value on/off |
| canToggle | boolean | Whether any values are available |
| isLoading | boolean | Loading state |
Each RefinementListItem has { value: string; count: number; isRefined: boolean }.
usePagination()
Pagination state derived from the current search results. Pages are 0-indexed.
const { currentPage, totalPages, totalHits, setPage, canPrevious, canNext, pages } =
usePagination();| Return | Type | Description |
| ------------- | ------------------------- | ------------------------------------ |
| currentPage | number | Current page index (0-based) |
| totalPages | number | Total number of pages |
| totalHits | number | Total result count |
| setPage | (page: number) => void | Navigate to a page (clamped) |
| canPrevious | boolean | Whether a previous page exists |
| canNext | boolean | Whether a next page exists |
| pages | number[] | Window of page numbers for rendering |
useStats()
Returns summary statistics about the current search.
const { totalHits, processingTimeMs, query, isLoading } = useStats();| Return | Type | Description |
| ----------------- | --------- | -------------------------------- |
| totalHits | number | Total results for the query |
| processingTimeMs| number | Server-side processing time (ms) |
| query | string | The query that produced results |
| isLoading | boolean | Loading state |
useSortBy(props)
Manages sort-by dropdown state.
const { currentSort, setSort, items } = useSortBy({
items: [
{ value: "price:asc", label: "Price (low to high)" },
{ value: "price:desc", label: "Price (high to low)" },
{ value: "rating:desc", label: "Top rated" },
],
});| Prop | Type | Description |
| ------- | -------------- | ----------------------------------------- |
| items | SortByItem[] | Available sort options |
Each SortByItem has { value: string; label: string }.
| Return | Type | Description |
| ------------- | --------------------------------- | ------------------------------ |
| currentSort | string \| null | Active sort value (null = relevance) |
| setSort | (value: string\|null) => void | Set the sort order |
| items | (SortByItem & { isSelected })[] | Items with isSelected flag |
useRange(props)
Numeric range filter for a given attribute.
const { min, max, stats, setRange, clear, isLoading } = useRange({
attribute: "price",
});| Prop | Type | Description |
| ----------- | -------- | ------------------------------------- |
| attribute | string | Numeric attribute (e.g. "price") |
| Return | Type | Description |
| ----------- | ------------------------------------------ | -------------------------------- |
| min | number \| undefined | Current min bound |
| max | number \| undefined | Current max bound |
| stats | { min: number; max: number } \| undefined| Overall min/max from engine |
| setRange | (range: { min?: number; max?: number }) => void | Set range filter |
| clear | () => void | Remove the range filter |
| isLoading | boolean | Loading state |
useHighlight(props)
Extracts the highlighted value for a specific attribute from a hit. Sanitizes the HTML to only allow <em> tags for safety.
const { value, highlighted } = useHighlight({
hit: someHit,
attribute: "title",
});| Prop | Type | Description |
| ----------- | ------------------------ | ---------------------------------- |
| hit | Record<string, unknown>| The hit object from useHits() |
| attribute | string | Attribute to highlight |
| Return | Type | Description |
| ------------- | --------- | ---------------------------------------------- |
| value | string | Highlighted HTML string (safe for innerHTML) |
| highlighted | boolean | Whether the value contains highlight marks |
Supports nested attributes via dot notation (e.g. "author.name").
useSearch()
Escape-hatch hook that exposes the full search state and all actions. Prefer the specialized hooks above for most use cases.
const {
query, results, isLoading, error, refinements, ranges, sortBy, page,
hitsPerPage, facets, setQuery, setPage, setSortBy, setHitsPerPage,
toggleRefinement, setRange, clearRange, registerFacet, unregisterFacet,
refresh,
} = useSearch();Components
All components accept a styled prop (default true) that toggles built-in Tailwind CSS classes. Set styled={false} to bring your own styles.
<SearchBox>
Search input with built-in clear button and search icon.
<SearchBox
placeholder="Search products..."
autoFocus
onQueryChange={(q) => console.log(q)}
/>| Prop | Type | Default | Description |
| --------------- | --------------------------- | -------------- | ---------------------------- |
| placeholder | string | "Search..." | Input placeholder |
| className | string | -- | Additional CSS classes |
| autoFocus | boolean | false | Auto-focus on mount |
| onQueryChange | (query: string) => void | -- | Called on every query change |
| styled | boolean | true | Enable built-in styles |
<Hits>
Renders search result hits in a grid layout.
<Hits hitComponent={ProductCard} emptyComponent={<p>No results.</p>} />| Prop | Type | Default | Description |
| ---------------- | ------------------------------- | -------------- | ------------------------------ |
| hitComponent | ComponentType<{ hit: Hit }> | JSON card | Custom component per hit |
| className | string | -- | CSS classes for the container |
| emptyComponent | ReactNode | "No results" | Shown when there are no hits |
| styled | boolean | true | Enable built-in styles |
<RefinementList>
Checkbox list of facet values with counts.
<RefinementList attribute="brand" header="Brand" limit={10} sortBy="count" />| Prop | Type | Default | Description |
| ----------- | -------------------- | --------- | -------------------------------- |
| attribute | string | -- | Facet attribute |
| limit | number | 20 | Max values to show |
| sortBy | "count" \| "alpha" | "count" | Value sort order |
| header | string | -- | Optional heading above the list |
| className | string | -- | Additional CSS classes |
| styled | boolean | true | Enable built-in styles |
<Pagination>
Page navigation with Previous/Next buttons and numbered pages.
<Pagination />| Prop | Type | Default | Description |
| ----------- | --------- | ------- | ------------------------ |
| className | string | -- | Additional CSS classes |
| styled | boolean | true | Enable built-in styles |
<Stats>
Displays result count and processing time (e.g. "1,234 results in 3ms").
<Stats />
<Stats render={({ totalHits, processingTimeMs }) =>
`Found ${totalHits} items (${processingTimeMs}ms)`
} />| Prop | Type | Default | Description |
| ----------- | ------------------------------------------------------- | ------- | ------------------------- |
| render | (stats: { totalHits; processingTimeMs; query }) => string | -- | Custom render function |
| className | string | -- | Additional CSS classes |
| styled | boolean | true | Enable built-in styles |
<SortBy>
Sort-by select dropdown.
<SortBy
items={[
{ value: "price:asc", label: "Price (low to high)" },
{ value: "price:desc", label: "Price (high to low)" },
]}
/>| Prop | Type | Default | Description |
| ----------- | -------------- | ----------- | -------------------------- |
| items | SortByItem[] | -- | Sort options |
| label | string | "Sort by" | Accessibility label |
| className | string | -- | Additional CSS classes |
| styled | boolean | true | Enable built-in styles |
<Highlight>
Renders highlighted text for a hit attribute. Safe for use with dangerouslySetInnerHTML -- all HTML except <em> tags is stripped and escaped.
<Highlight hit={hit} attribute="title" />| Prop | Type | Default | Description |
| ----------- | ------------------------- | -------- | ----------------------------- |
| hit | Record<string, unknown> | -- | Hit object from useHits() |
| attribute | string | -- | Attribute to highlight |
| tagName | keyof HTMLElementTagNameMap | "span" | Wrapper element tag |
| className | string | -- | Additional CSS classes |
| styled | boolean | true | Enable built-in styles |
<RangeInput>
Min/max number inputs for numeric range filtering.
<RangeInput attribute="price" header="Price" />| Prop | Type | Default | Description |
| ---------------- | --------- | ------- | ----------------------------- |
| attribute | string | -- | Numeric attribute to filter |
| header | string | -- | Optional heading |
| minPlaceholder | string | "Min" | Min input placeholder |
| maxPlaceholder | string | "Max" | Max input placeholder |
| className | string | -- | Additional CSS classes |
| styled | boolean | true | Enable built-in styles |
<PoweredBy>
"Powered by Headband" badge with link.
<PoweredBy />| Prop | Type | Default | Description |
| ----------- | --------- | ------------------------ | ------------------------ |
| href | string | "https://headband.dev" | Link URL |
| className | string | -- | Additional CSS classes |
| styled | boolean | true | Enable built-in styles |
Comparison to Algolia InstantSearch
| Feature | @headband/react | react-instantsearch |
| --------------------- | ---------------------------- | ---------------------------- |
| Provider | <HeadbandProvider> | <InstantSearch> |
| Search input | <SearchBox> / useSearchBox | <SearchBox> / useSearchBox |
| Results | <Hits> / useHits | <Hits> / useHits |
| Facets | <RefinementList> | <RefinementList> |
| Pagination | <Pagination> | <Pagination> |
| Stats | <Stats> | <Stats> |
| Sort | <SortBy> | <SortBy> |
| Range | <RangeInput> / useRange | <RangeInput> / useRange |
| Highlighting | <Highlight> | <Highlight> |
| Bundle size | ~33 KB (ESM, unminified) | ~120 KB+ |
| Backend lock-in | None (Headband is engine-agnostic) | Algolia only |
| Built-in styles | Tailwind (opt-out with styled={false}) | CSS classes |
TypeScript
All hooks, components, and types are fully typed and exported. Import types directly:
import type {
HeadbandClient,
SearchParams,
SearchResponse,
Hit,
RefinementListItem,
SortByItem,
} from "@headband/react";License
MIT
