Search

Use useSearch with DataTable and whatever input pattern fits the surrounding table UI.

Overview

DataTable does not own search state. useSearch owns the query, debounce, matching strategy, and result ordering. The usual pattern is:

  • useSearch owns query state and local matching
  • your input UI reads from search.inputProps
  • DataTable receives search.results ?? data

DataTable never performs the matching itself. It receives rows that useSearch has already filtered and ordered.

Matching Modes

useSearch supports different matching strategies depending on how exact or forgiving the search should feel.

"includes" is the default. It performs a case-insensitive substring match across the configured keys.

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

Use this when you want predictable string matching and stable original ordering.

Fuzzy Search and Relevance

Use "fuzzy" when you want typo-tolerant matching and relevance ordering.

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

const rows = search.results ?? data;

With fuzzy search, search.results is already ordered by relevance. If you also need explicit rank metadata, use search.rankedResults.

search.rankedResults?.map((result) => ({
  row: result.item,
  rank: result.rank,
}));

Search With Column Filters

Treat search as the first pass, then let structured column filters run on the already matched rows.

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

const filterDefs = defineFilters([
  {
    key: "status",
    label: "Status",
    type: "select",
  },
  {
    key: "department",
    label: "Department",
    type: "select",
  },
] as const);

const tableFilters = useFiltersTanstack({
  definition: filterDefs,
  columns: tableColumns,
  data: search.results ?? data,
  strategy: "client",
});

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 /> : null}
      </Button>
    </SearchExpand>

<Filters
  fields={tableFilters.filterFields}
  filters={tableFilters.filters}
  onChange={tableFilters.handleFiltersChange}
/>
<DataTable
  columnFilters={tableFilters.columnFilters}
  columns={tableFilters.columns}
  data={search.results ?? data}
  enableFiltering
  onColumnFiltersChange={tableFilters.onColumnFiltersChange}
/>
  </>
);

This keeps free-text search separate from the column-filter model while still composing cleanly into the same table.

API Reference

Common useSearch Options

OptionTypeDescription
itemsT[]Source rows to search locally
keysSearchKey<T>[]Keys or selectors searched for local matching
strategy"includes" | "fuzzy" | "custom"Search strategy. Defaults to "includes"
debounceMsnumberDelay query updates while typing
minQueryLengthnumberMinimum trimmed query length before filtering starts
limitnumberMaximum number of returned results
paramNamestringOptional URL parameter used for query state

Common Return Fields

FieldTypeDescription
inputPropsobjectControlled props to spread into your input
valuestringImmediate query value
debouncedValuestringDebounced query value
resultsT[] | undefinedFiltered and ordered rows for rendering
rankedResultsSearchResult<T>[] | undefinedRanked matches with relevance metadata
clear() => voidClear the current query
setValue(value: string) => voidImperatively set the query
isEmptybooleanWhether the current query string is empty
hasQuerybooleanWhether the trimmed query is non-empty

See useSearch for the full hook API. This page covers the DataTable composition pattern rather than the entire hook surface.

On this page