TreeView#

TreeView renders hierarchical data with expand/collapse toggles, indentation, and virtualization: only visible rows are rendered. Maps to UI Toolkit's TreeView.

Import#

import { TreeView } from "onejs-react"

Basic Usage#

Data is a plain nested array. Each node has data (whatever you want), optional children, and an optional numeric id. renderItem returns the JSX for one row and receives the node's data directly:

const project = [
    {
        id: 1, data: "Assets", children: [
            { id: 2, data: "Player.cs" },
            { id: 3, data: "Enemy.cs" },
        ],
    },
    { id: 4, data: "README.md" },
]

function FileTree() {
    return (
        <TreeView
            rootItems={project}
            fixedItemHeight={22}
            autoExpand
            renderItem={(name: string) => <Label text={name} style={{ flexGrow: 1 }} />}
            style={{ height: 300 }}
        />
    )
}

The toggle and the indentation come from UI Toolkit, so renderItem returns only the row's content. A richer row is just more JSX:

renderItem={(node: FileNode) => (
    <View style={{ flexDirection: "row", alignItems: "center", flexGrow: 1 }}>
        <View style={{ width: 12, height: 12, borderRadius: 3, marginRight: 8, backgroundColor: node.color }} />
        <Label text={node.name} style={{ fontSize: 14, flexGrow: 1 }} />
        <Label text={node.meta} style={{ fontSize: 11, color: "#8b94a6" }} />
    </View>
)}

renderItem needs onejs-react 0.1.56 or newer.

Ids and Updating Data#

Ids identify nodes across data updates: selection and expansion state are tracked by id. They are optional (omitted ids are auto-assigned), but provide stable ids whenever the tree's data changes over time, or state will not carry over.

To update the tree, pass a new rootItems reference. Mutating the existing array does nothing, the same rule as React state in general:

const [items, setItems] = useState(initialItems)

// Adds a node: new arrays along the changed path
setItems([...items, { id: 99, data: "New.txt" }])

Props#

Required Props#

PropTypeDescription
rootItemsTreeViewItem[]Nested data: { id?, data, children? }
renderItem(data, index) => ReactNodeJSX for one row. Use this, or the imperative pair below, not both

Row Props (imperative alternative)#

PropTypeDescription
makeItem() => VisualElementFactory for row content elements
bindItem(element, index, data) => voidPopulate a row; data is the node's data
unbindItem(element, index, data) => voidCalled when a row is recycled
destroyItem(element) => voidCalled when a row element is retired

Optional Props#

PropTypeDefaultDescription
fixedItemHeightnumberFixed row height (fastest)
virtualizationMethod"FixedHeight", "DynamicHeight""FixedHeight"Virtualization strategy
autoExpandbooleanfalseExpand all items when data is set
selectionType"None", "Single", "Multiple""Single"Selection mode
onSelectionChange(items, ids) => voidSelected node data + ids
showBorderbooleanfalseBorder around the tree
showAlternatingRowBackgrounds"None", "ContentOnly", "All""None"Zebra striping

Rows recycle as the tree scrolls, exactly as ListView's do, and renderItem keeps that while letting React own what is inside a row. How Rows Work on the ListView page covers what recycling means for state inside a row; it reads the same here.

Selection#

onSelectionChange hands you the selected nodes' data and their ids:

function SelectableTree({ items }) {
    const [current, setCurrent] = useState(null)

    return (
        <View>
            <Label text={current ? `Selected: ${current.name}` : "Nothing selected"} />
            <TreeView
                rootItems={items}
                fixedItemHeight={24}
                selectionType="Single"
                onSelectionChange={(datas, ids) => setCurrent(datas[0] ?? null)}
                renderItem={(node: FileNode) => <Label text={node.name} style={{ flexGrow: 1 }} />}
                style={{ height: 400 }}
            />
        </View>
    )
}

Expand, Collapse, and Selection by Id#

The underlying UI Toolkit methods are available through a ref:

function Controls() {
    const tree = useRef(null)

    return (
        <View>
            <View style={{ flexDirection: "row" }}>
                <Button text="Expand All" onClick={() => tree.current.ExpandAll()} />
                <Button text="Collapse All" onClick={() => tree.current.CollapseAll()} />
                <Button text="Jump" onClick={() => tree.current.SetSelectionById(3)} />
            </View>
            <TreeView ref={tree} rootItems={items} ... />
        </View>
    )
}

Useful methods: ExpandAll(), CollapseAll(), ExpandItem(id), CollapseItem(id), IsExpanded(id), SetSelectionById(id), GetTreeCount().

Imperative Rows#

makeItem and bindItem are the older API, and still the right one when a row is a single element whose text you want to set with no React in the path. The element is recycled, so bindItem must set every property it cares about:

<TreeView
    rootItems={project}
    fixedItemHeight={22}
    makeItem={() => {
        const label = new CS.UnityEngine.UIElements.Label()
        label.style.flexGrow = 1
        return label
    }}
    bindItem={(element, index, data) => {
        element.text = data
    }}
/>

These are raw C# elements rather than React ones, so styles want real structs (new CS.UnityEngine.Color(...), not a colour string), and children are reached by position with element.ElementAt(i). Q(name) also works after a one-time useExtensions(CS.UnityEngine.UIElements.UQueryExtensions) (OneJS 3.1.3+). See ListView: Imperative Rows for the same notes in full.

Pass renderItem or the imperative pair, never both. TypeScript rejects a TreeView that has both.

Styling the Built-in Chrome#

The expand toggle, indentation, and row highlights come from UI Toolkit's default theme, which assumes a light-friendly palette (the default hover is a light grey that fights light text on dark UIs). Restyle them with a small stylesheet scoped under a class on your TreeView:

compileStyleSheet(`
.my-tree .unity-tree-view__item-toggle {
    margin-right: 6px;
}
.my-tree .unity-tree-view__item-toggle .unity-toggle__checkmark {
    background-color: rgba(0, 0, 0, 0);
    -unity-background-image-tint-color: #8b93a7;
}
.my-tree .unity-collection-view__item:hover {
    background-color: rgba(91, 156, 248, 0.12);
}
.my-tree .unity-collection-view__item--selected,
.my-tree .unity-collection-view__item--selected:hover {
    background-color: #2f4a7a;
}
`, "my-tree")
<TreeView className="my-tree" ... />

Rows are .unity-collection-view__item, with a --selected modifier and a :hover state; the expand toggle is .unity-tree-view__item-toggle, and the arrow inside it is .unity-toggle__checkmark (clear its background for a bare chevron). ListView rows share the same unity-collection-view__item classes, so the row rules work there too.

TreeView or ListView?#

Use TreeView when the data is hierarchical and users need to fold branches. For flat data, ListView is simpler and slightly cheaper. Both virtualize, so either handles thousands of rows.