TreeSelect

A Select whose options are a tree: the field opens a hierarchy rather than a flat list, with checkable nodes for multiple values.

Usage

When to reach for TreeSelect rather than a Cascader, a Select or a Tree, and the handful of props that decide what its value contains.

When to use

TreeSelect is a Select whose options are a hierarchy. The field reads as a form control, closed, showing whatever is chosen. The structure only appears when the popup opens, and it exists so the reader can find one node among many, not so they can study the shape of the tree.
Reach for it when all three are true:
  • The answer is a field's value: a category, a folder, a department, a location. Something a form submits.
  • The options nest, and the nesting is what makes them findable. “Engineering / Platform / Storage” is easier to reach through its parents than out of a list of two hundred.
  • A branch can be a legitimate answer, or the depth is ragged. Choosing “Engineering” and choosing “Storage” are both valid, and not every path runs the same number of levels deep.
What to use instead
  • Cascader when every path has the same depth and the reader always ends on a leaf. Cascader walks the levels one column at a time and treats the path itself as the value, which is wrong for a tree where a branch may be the answer.
  • Select when the options are flat, or when the grouping is decoration rather than navigation. Two dozen options under four headings is a Select with options groups, not a tree.
  • Tree when the hierarchy is the page: a file browser, a permissions matrix, a navigation rail. A Tree is a surface the reader works in. A TreeSelect is a field they answer and leave.
  • Transfer with TreeSelect's job split in two when the reader has to see everything they picked at once. A field that collapses back to tags hides the answer. Two panes do not.

treeData and fieldNames

treeData is the API. TreeSelect.TreeNode is deprecated, and the kit does not export it. Give every node a key and a value: the node type requires both.
  • The default keys are value, children, and title for the label. A node can carry its label as title or label.
  • fieldNames renames those three rather than reshaping your data: { value: "id", label: "name", children: "items" } reads your own API straight. Renaming value also renames the key.
  • Values have to be unique across the whole tree, not just among siblings. Two nodes sharing a value are one node as far as selection is concerned, and nothing warns.
  • An inline fieldNames object is safe. An inline treeData is not: build it outside the render, or memoize it, because rebuilding the array on every keypress rebuilds the node index behind it.
  • treeTitleRender decorates a row in the popup without touching the data. What it returns is not what the closed field shows, so pair it with treeNodeLabelProp when the two should differ.

treeCheckable and showCheckedStrategy

treeCheckable puts a checkbox on every node and, in doing so, decides two other things for you. It forces multiple on (passing multiple={false} with it logs a warning), and it makes parents and children imply each other: checking a branch checks its leaves, and checking every leaf fills in the branch.
showCheckedStrategy then decides what a fully checked branch reports. It changes the value, not the picture.
  • SHOW_CHILD is the default and reports the leaves. Reach for it when the consumer of the value is a filter or a query: it wants ids it can match, and a parent id means nothing to it.
  • SHOW_PARENT collapses a filled branch to the ancestor. Reach for it when the value is stored as a rule (“everyone in Engineering”) and should keep meaning that after somebody adds a new team under it.
  • SHOW_ALL reports both, which is the right answer only when the receiving end needs the whole checked set.
All three constants hang off the component itself, so TreeSelect.SHOW_PARENT needs no second import. maxCount needs the default strategy: with treeCheckable it is ignored under SHOW_PARENT or SHOW_ALL unless treeCheckStrictly is on.

treeCheckStrictly changes the value's shape

treeCheckStrictly cuts the link between a parent and its children: each node is checked on its own, and a branch with some of its leaves checked renders half-checked rather than filling in. That much is a behavior change. The part that breaks code is the second one.
  • It turns labelInValue on and there is no way to turn it back off. Every entry in the value becomes { value, label } instead of a bare key. A form that was reading string[] starts receiving objects.
  • The second argument to onChange, the label list, is null whenever the value is labeled. The labels are inside the value, so they are no longer passed beside it.
  • An entry can carry halfChecked: true, which exists only in this mode. Decide what a half-checked ancestor means to your backend before you turn the flag on.
  • A controlled value has to be written in the same shape you receive. Passing plain keys back into a strict TreeSelect leaves the labels blank until the tree resolves them.
labelInValue on its own does the same thing to the value without changing the checking behavior. Reach for it when the label has to survive a round trip, for instance when the chosen node may not be in the treeData you load next time.

multiple, and how many tags to show

multiple without treeCheckable gives tags and no checkboxes: each pick is independent, nothing cascades, and a branch is just another option. It suits a value that is a set of labels rather than a region of the tree.
  • maxTagCount caps what the closed field draws. A number is a hard cap. responsive hides whatever does not fit on one line, which is what keeps a field inside a form grid from growing three rows tall while the reader picks.
  • The overflow is summarized by maxTagPlaceholder, whose default is the literal + 3 .... Write your own as soon as the count matters: it takes the hidden values, so “+3 more teams” costs one line.
  • maxTagTextLength truncates each tag's own text. Use it before you use a narrower field: a tag cut to eight characters is still a tag the reader can remove.
  • maxCount is a different prop with a different job: it refuses further selections rather than hiding them, and it disables the remaining nodes once the cap is reached.
  • Backspace in an empty search box removes the last tag, skipping disabled entries.
Searching is off until you ask for it, and the default it comes with is usually not the one you want.
  • treeNodeFilterProp defaults to "value", not to the label. On any tree whose values are ids, typing a word matches nothing at all. Set it to the field the reader can see, usually "title".
  • showSearch takes a config object as well as a boolean: { treeNodeFilterProp: "title", autoClearSearchValue: false }. The flat props of the same name still work and are deprecated, so prefer the object in new code.
  • filterTreeNode replaces the match entirely when a substring is not enough, for example to search a node's full path or its synonyms. Returning false from it turns filtering off while leaving the input in place, which is how you hand searching to a server.
  • autoClearSearchValue is true: the box empties after every pick. Turn it off when the reader is checking several nodes that match the same query, so they do not retype it for each pick.
  • A match forces its ancestors open, so the popup re-expands while the reader types. That is why controlled expansion and search need to be designed together rather than bolted on in either order.

Expanding, and loading as you go

treeDefaultExpandAll is the convenient answer and the one that does not scale. It opens every branch on mount, which means the popup lays out every node in the tree before the reader has typed a character.
  • Past a few dozen nodes, control it: treeExpandedKeys with onTreeExpand. You then decide what opens on mount, what survives a re-open, and what search is allowed to expand.
  • treeDefaultExpandedKeys is the uncontrolled middle ground: open the one branch the reader most likely wants and leave the rest shut.
  • virtual is on by default and windows the popup's rows, so pair it with a listHeight (256 by default) that matches the surface. Virtualization switches itself off when popupMatchSelectWidth is false, which is an easy way to lose it without noticing.
  • loadData fills a branch when it opens: return a promise, and resolve it after the children are in treeData. Mark the parents that have children with isLeaf: false so the switcher is there to click in the first place.
  • Load children into flat rows. With loadData, use treeDataSimpleMode rows with an id and a pId, and append the new children to state. Changing the node loadData receives does not update the tree. loadData can run twice for one node, so skip a node whose children are already there.
  • loadData does not run while a search is active. It is switched off as soon as there is a search value, so a lazily loaded tree can only be searched over the part of it that has already arrived. Either load eagerly or search on the server.
  • treeExpandAction is false by default: only the switcher opens a branch. Set it to "click" when branches are not selectable anyway, so the whole row is a target rather than a 24px arrow.

Styling it

Reach for the classNames and styles keys before a descendant selector: root, prefix, input, suffix, content, placeholder, item, itemContent, itemRemove, and popup, which nests its own root, item, itemTitle and itemSwitcher. A keyed part stays stable across versions, and a class name may not.
The tree renders outside this component's root element, in a portal, so a [&_.ant-select-tree] selector on the field reaches nothing. The nested popup object is the route in, the same shape Select has.
The icons follow the icon pack. The arrow, the chip remove and the clear icons are set by TreeSelect itself, and the expand arrows in the tree come from AntuikitProvider. Pass null as suffixIcon to hide the arrow. A null removeIcon shows a plain × instead of an icon.
Set right-to-left on the provider. Under <ThemeScope direction="rtl"> the expand arrows in the dropdown keep pointing the left-to-right way, because the dropdown renders outside the scope. Set direction on AntuikitProvider instead.