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.
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']
}
});