Skip to main content

Headless hook

useHighlight returns { segments, getMatchCount } — the same matching pipeline as <Highlight>, but with no rendering opinions. Reach for the hook when you need to wrap each match in something <Highlight>'s renderMatch can't easily express:

  • a <button> per match so clicking jumps the user to it
  • a <a href="#match-3"> for shareable deep-links into long documents
  • table cells, list items, or virtualized rows
  • analytics attributes (data-match-index, data-states) on each segment
  • non-<mark> semantic tags

The hook returns the structured segments; you stay in charge of the JSX.

3 matches — middle one is the "current" hit

A common React mistake: calling with no dependency array makes it run after every render. Use the deps array to control when fires, and return a cleanup function from to undo subscriptions.

Open in StackBlitz

Usage

import { useHighlight } from 'one-more-highlight';

function MyHighlighter({ text, query }: { text: string; query: string }) {
const { segments } = useHighlight({ text, searchWords: [query] });

return (
<p>
{segments.map((s, i) =>
s.isMatch
? <mark key={i}>{s.text}</mark>
: <span key={i}>{s.text}</span>
)}
</p>
);
}

useHighlight accepts all the same options as <Highlight> minus the rendering props (highlightTag, highlightClassName, renderMatch, etc.).

Segment types

type Segment = MatchSegment | TextSegment;

interface MatchSegment {
text: string;
isMatch: true;
matchIndex: number; // 0-based document order
start: number; // character index in original text
end: number;
states: ReadonlyArray<string>; // names of active states
}

interface TextSegment {
text: string;
isMatch: false;
start: number;
end: number;
}

Segments are guaranteed to be contiguous and cover the full input text with no gaps.

With states

const { segments, getMatchCount } = useHighlight({
text,
searchWords: ['time'],
states: [
{ name: 'active', index: 2 },
{ name: 'bookmarked', indices: [0, 3] },
],
});

console.log(getMatchCount()); // e.g. 8

segments.forEach(s => {
if (s.isMatch) {
console.log(s.matchIndex, s.states);
// e.g. 2, ['active']
// e.g. 0, ['bookmarked']
}
});