useSearch

Compose query state, debounce, URL sync, and optional local search results.

useSearch is the reusable search state hook for Cubitt. It can run in query-only mode or local results mode.

Use it when you want one place to manage:

  • the live input value
  • the debounced value
  • clear/reset behavior
  • optional URL sync
  • optional includes or fuzzy matching against local items

Query-Only Usage

import { Button } from "@tilt-legal/cubitt-components/button";
import { Input } from "@tilt-legal/cubitt-components/input";
import { SearchExpand } from "@tilt-legal/cubitt-components/search-expand";
import { useSearch } from "@tilt-legal/cubitt-components/utilities/hooks";
import { Xmark } from "@tilt-legal/cubitt-icons/ui/outline";

function ToolbarSearch() {
  const search = useSearch({
    debounceMs: 100,
    paramName: "q",
  });

  return (
    <SearchExpand expandedWidth="20rem">
      <Input placeholder="Search..." {...search.inputProps} />
      <Button
        disabled={search.isEmpty}
        mode="icon"
        onClick={search.clear}
        size="sm"
        variant="link"
      >
        {!search.isEmpty && <Xmark />}
      </Button>
    </SearchExpand>
  );
}

In this mode, useSearch manages input state, debounce, and URL synchronization but does not calculate local results.

const search = useSearch({
  items,
  keys: ["name", "email"],
  debounceMs: 100,
});

const visibleItems = search.results ?? items;

The default strategy is "includes", which does a case-insensitive substring match across the configured keys.

const search = useSearch({
  strategy: "fuzzy",
  items,
  keys: ["name", "email", (item) => item.department],
  debounceMs: 100,
  paramName: "q",
});

const visibleItems = search.results ?? items;

search.results is already ordered by relevance. If you need metadata as well, use search.rankedResults.

DataTable Integration

Use useSearch outside the table and pass the filtered rows into DataTable.

const search = useSearch<Row>({
  strategy: "fuzzy",
  items: data,
  keys: ["name", "email", "department"],
  debounceMs: 100,
});

<DataTable
  columns={columns}
  data={search.results ?? data}
  enableSorting
/>

This keeps search reusable and avoids a table-specific search component.

API

Options

OptionTypeDefaultDescription
valuestring-Controlled search value
defaultValuestring""Initial value for uncontrolled mode
onValueChange(value: string) => void-Called when the value changes
debounceMsnumber0Debounce delay before debouncedValue updates
itemsreadonly TItem[]-Optional local dataset to search
strategy"includes" | "fuzzy" | "custom""includes"Search strategy to apply
keysSearchKey<TItem>[]-Keys or selectors used to derive searchable values
matcher(item: TItem, query: string) => boolean-Required when strategy is "custom"
limitnumber-Maximum number of returned results
minQueryLengthnumber0Minimum trimmed query length before filtering starts
paramNamestring-URL query parameter key
paramClearOnDefaultbooleantrueRemove the param when the value matches the default
paramThrottlenumber-Throttle URL updates
paramDebouncenumber-Debounce URL updates
onUrlValueChange(value: string | null) => void-Callback when the URL value changes

Return Value

FieldTypeDescription
valuestringImmediate input value
debouncedValuestringDebounced query value
setValue(value: string) => voidImperatively update the query
clear() => voidReset the query to an empty string
isEmptybooleantrue when value === ""
hasQuerybooleantrue when the trimmed value is non-empty
inputProps{ value; onChange }Controlled props for a composed input
resultsTItem[] | undefinedOrdered visible items when items was provided
rankedResultsSearchResult<TItem>[] | undefinedOrdered results plus rank metadata

Notes

  • When useSearch owns URL sync, do not also pass URL-state props directly to the composed Input.
  • useSearch does not render UI. It is designed to compose with SearchExpand, Input, Autocomplete, or custom inputs.
  • matcher is only used for "custom" strategy.

On this page