The descendants pattern is used to coordinate parent and child components (like List and List.Item) for features like keyboard navigation and selection.
The useDescendant hook
Returns an object with:
index: The current position in the rendered list (subject to change via filtering/reordering).descendantId: A stable, unique ID for the item.
CRITICAL: Always use descendantId for tracking item-specific state (selection, expansion, etc.). Using index for state tracking is incorrect because indices change when items are filtered or reordered.
// CORRECT: Using descendantId for selection tracking
const isSelected = selectedIds.has(descendant.descendantId)
// WRONG: Using index for selection tracking
// const isSelected = selectedIndexes.has(descendant.index)
Accessing map.current
When using useDescendants(), the map.current property is NOT reactive.
- DO NOT access
map.current during render or inside useMemo. - DO access
map.current inside useEffect, useLayoutEffect, or event handlers (e.g., useKeyboard).
To handle filtering, do not use map.current to filter the list; instead, have the child components render null and pass the search query via context.
import { useKeyboard } from '@opentui/react'
import { useDescendants } from 'termcast/src/descendants'
const { map } = useDescendants()
useKeyboard((evt) => {
// Access map.current during useEffect or event handlers, NOT during render
const items = Object.values(map.current)
.filter((item) => item.index !== -1)
.sort((a, b) => a.index - b.index)
.map((item) => item.props)
// Handle your logic with items, e.g. navigating with up/down
})