Search + Select + Submit
Filter a tree, multi-select with checkboxes, click submit, and see exactly what got picked — arguably the single most common real-world checkbox-tree pattern.
Show code
import { useMemo, useState } from "react";
import { TreeKit, buildTreeIndex, getNodesByIds } from "@treekit-ui/core";
const index = buildTreeIndex(data);
// Cascading means checkedIds can include every descendant of a fully-
// checked branch — collapse to just the topmost checked id per branch
// for a clean results summary.
function getTopmostCheckedIds(index, checkedIds) {
const checkedSet = new Set(checkedIds);
return checkedIds.filter((id) => {
const parentId = index.parentIdById.get(id);
return !parentId || !checkedSet.has(parentId);
});
}
export function TeamPicker() {
const [search, setSearch] = useState("");
const [checkedIds, setCheckedIds] = useState([]);
const [submitted, setSubmitted] = useState(null);
const submittedNodes = useMemo(
() => (submitted ? getNodesByIds(index, getTopmostCheckedIds(index, submitted)) : []),
[submitted],
);
return (
<>
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search teams…" />
<TreeKit data={data} searchValue={search} checkedIds={checkedIds} onCheckedChange={setCheckedIds} />
<button disabled={checkedIds.length === 0} onClick={() => setSubmitted(checkedIds)}>
Notify selected teams
</button>
{submitted && submittedNodes.map((node) => <span key={node.id}>{node.label}</span>)}
</>
);
}Search for "back" to narrow down to Backend and its children, check a couple of nodes, then hit submit — everything below the search input and the button is just three pieces of state (search, checkedIds, submitted) and two TreeKit props (searchValue, checkedIds/onCheckedChange). No custom filtering logic, no manual cascade math.
Why the results list isn't just checkedIds
Check "Engineering" and cascading selects all 11 nodes underneath it — checkedIds correctly reflects that internally, but a results summary showing all 11 names is noisy when the user's actual intent was "the whole Engineering org." The demo above collapses the checked set down to just the topmost checked id per branch before rendering it, using the tree index TreeKit already builds internally:
function getTopmostCheckedIds(index, checkedIds) {
const checkedSet = new Set(checkedIds);
return checkedIds.filter((id) => {
const parentId = index.parentIdById.get(id);
return !parentId || !checkedSet.has(parentId);
});
}This is a general pattern, not demo-only code
buildTreeIndex and getNodesByIds are both exported from @treekit-ui/core — see Utilities. Building the index once (outside your component, since data is static here) keeps this O(1) per lookup instead of re-walking the tree on every submit.