useTreeFilter
Filter hook for hierarchical data with lenient and strict branch-matching semantics.
Overview#
useTreeFilter walks hierarchical data (nodes with children) and returns the matching branches. It shares the same rule shape and matcher registry as useFilter, so switching between flat and tree data is only a matter of which hook you call.
import { useTreeFilter } from '@primereact/hooks/use-tree-filter';filterMode#
lenient(default) — a branch is kept if it matches directly or any descendant matches. The whole subtree comes along.strict— leaves must match themselves; parents are kept only when a descendant matches. Non-matching leaves never appear.
Lenient: a branch is kept if any descendant matches. Strict: leaves must match; parents are kept only when a descendant matches.
'use client';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Search } from '@primeicons/react/search';
import { useTreeFilter, type TreeFilterMode } from '@primereact/hooks/use-tree-filter';
import { IconField } from '@primereact/ui/iconfield';
import { InputText } from '@primereact/ui/inputtext';
import { ToggleButton } from '@primereact/ui/togglebutton';
import { ToggleButtonGroup } from '@primereact/ui/togglebuttongroup';
import * as React from 'react';
interface Node {
key: string;
data: { name: string; type: 'folder' | 'file' };
children?: Node[];
}
const tree: Node[] = [
{
key: '0',
data: { name: 'Documents', type: 'folder' },
children: [
{ key: '0-0', data: { name: 'Work Report.docx', type: 'file' } },
{ key: '0-1', data: { name: 'Expenses.xlsx', type: 'file' } },
{
key: '0-2',
data: { name: 'Invoices', type: 'folder' },
children: [
{ key: '0-2-0', data: { name: 'January.pdf', type: 'file' } },
{ key: '0-2-1', data: { name: 'February.pdf', type: 'file' } }
]
}
]
},
{
key: '1',
data: { name: 'Media', type: 'folder' },
children: [
{ key: '1-0', data: { name: 'Holiday.mp4', type: 'file' } },
{ key: '1-1', data: { name: 'Wallpaper.jpg', type: 'file' } }
]
},
{
key: '2',
data: { name: 'Archive', type: 'folder' },
children: [{ key: '2-0', data: { name: 'old-work-report.docx', type: 'file' } }]
}
];
function renderNode(node: Node, level = 0): React.ReactNode {
return (
<React.Fragment key={node.key}>
<div
className="flex items-center gap-2 py-1 px-2 rounded hover:bg-surface-100 dark:hover:bg-surface-800"
style={{ paddingInlineStart: `${0.5 + level * 1.25}rem` }}
>
{node.data.type === 'folder' ? (
<Folder className="w-4 h-4 text-yellow-500" />
) : (
<File className="w-4 h-4 text-surface-500 dark:text-surface-400" />
)}
<span className={node.data.type === 'folder' ? 'font-semibold' : ''}>{node.data.name}</span>
</div>
{node.children?.map((c) => renderNode(c, level + 1))}
</React.Fragment>
);
}
export default function TreeDemo() {
const [mode, setMode] = React.useState<TreeFilterMode>('lenient');
const { filteredNodes, rules, setRules } = useTreeFilter<Node>({
nodes: tree,
defaultRules: [{ field: 'name', value: null, matchMode: 'contains' }],
filterMode: mode
});
const search = (rules[0]?.value as string) ?? '';
return (
<div className="w-full flex flex-col gap-3">
<div className="flex items-center gap-2">
<IconField.Root>
<IconField.Inset>
<Search />
</IconField.Inset>
<InputText
type="search"
placeholder="Search nodes..."
value={search}
onChange={(e: any) => setRules([{ field: 'name', value: e.target.value, matchMode: 'contains' }])}
/>
</IconField.Root>
<ToggleButtonGroup
allowEmpty={false}
multiple={false}
value={mode}
onValueChange={(e: { value: unknown }) => setMode(e.value as TreeFilterMode)}
>
<ToggleButton.Root value="lenient" size="small">
<ToggleButton.Indicator>Lenient</ToggleButton.Indicator>
</ToggleButton.Root>
<ToggleButton.Root value="strict" size="small">
<ToggleButton.Indicator>Strict</ToggleButton.Indicator>
</ToggleButton.Root>
</ToggleButtonGroup>
</div>
<div className="border border-surface-200 dark:border-surface-700 rounded-md p-2 min-h-60">
{filteredNodes.length === 0 ? (
<div className="p-4 text-center text-sm text-surface-500 dark:text-surface-400">No matches.</div>
) : (
filteredNodes.map((n) => renderNode(n))
)}
</div>
<p className="text-xs text-surface-500 dark:text-surface-400">
<strong>Lenient</strong>: a branch is kept if any descendant matches. <strong>Strict</strong>: leaves must match; parents are kept
only when a descendant matches.
</p>
</div>
);
}
Rules#
Same shape as useFilter. For a node like { key, data: { name, type }, children }, field: 'name' resolves against node.data.name.
useTreeFilter({
nodes,
defaultRules: [{ field: 'name', value: 'doc', matchMode: 'contains' }],
filterMode: 'lenient'
});Lazy#
lazy: true skips the client-side tree walk; filteredNodes is returned as-is. onLazyLoad fires with the current rules plus the active filterMode — pass both to your backend so it honours the same branch-matching semantics.
useTreeFilter({
nodes,
lazy: true,
defaultRules: [{ field: 'label', value: null }],
filterMode: 'lenient',
onLazyLoad: ({ rules, filterMode }) => {
// fetch filtered tree from the server
}
});Custom matchers#
Use the same matchers option and the shared registerMatcher/unregisterMatcher helpers exposed from @primereact/hooks/use-filter.