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:
useSearchowns query state and local matching- your input UI reads from
search.inputProps DataTablereceivessearch.results ?? data
import { DataTable } from "@tilt-legal/cubitt-components/data-table";
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 SearchableTable({ data, columns }: Props) {
const search = useSearch<Row>({
items: data,
keys: ["name", "email", "department"],
strategy: "fuzzy",
debounceMs: 100,
});
return (
<>
<SearchExpand expandedWidth="20rem">
<Input placeholder="Search rows..." {...search.inputProps} />
<Button
disabled={search.isEmpty}
mode="icon"
onClick={search.clear}
size="sm"
variant="link"
>
{!search.isEmpty ? <Xmark /> : null}
</Button>
</SearchExpand>
<DataTable columns={columns} data={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 Search
"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
| Option | Type | Description |
|---|---|---|
items | T[] | Source rows to search locally |
keys | SearchKey<T>[] | Keys or selectors searched for local matching |
strategy | "includes" | "fuzzy" | "custom" | Search strategy. Defaults to "includes" |
debounceMs | number | Delay query updates while typing |
minQueryLength | number | Minimum trimmed query length before filtering starts |
limit | number | Maximum number of returned results |
paramName | string | Optional URL parameter used for query state |
Common Return Fields
| Field | Type | Description |
|---|---|---|
inputProps | object | Controlled props to spread into your input |
value | string | Immediate query value |
debouncedValue | string | Debounced query value |
results | T[] | undefined | Filtered and ordered rows for rendering |
rankedResults | SearchResult<T>[] | undefined | Ranked matches with relevance metadata |
clear | () => void | Clear the current query |
setValue | (value: string) => void | Imperatively set the query |
isEmpty | boolean | Whether the current query string is empty |
hasQuery | boolean | Whether 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.