Skip to main content

Controlled usage

There are two ways to give TreeSelect its data:

PropModeWho owns the selection
defaultDataUncontrolledTreeSelect. It starts from the data passed on the first render (later changes to defaultData are ignored), applies each selection itself, and reports it through onChange.
dataControlledYou. TreeSelect renders exactly the data it receives, and only reports changes through onChange.

Controlled example​

Keep the tree in state and pass onChange your state setter:

Root 1
Child 1
Grandchild 1
Grandchild 2
Child 2
Root 2
Child 3
Child 4
Selected IDs: []
Show code
ControlledExample.tsx
import { useState } from 'react';
import TreeSelect, { getSelectedIDs, SelectionMode } from '@attebyte/react-tree-select';
import { exampleData, ExampleNode } from './data';

export default function ControlledExample() {
const [data, setData] = useState(exampleData);
const [mode, setMode] = useState<SelectionMode>('leaves');

const selectedIDs = getSelectedIDs(data, {
uniqueIDProperty: 'value',
childrenProperty: 'children',
valueProperty: 'included',
mode
});

return (
<div className='flex flex-wrap gap-6'>
<TreeSelect<ExampleNode>
data={data}
onChange={setData}
uniqueIDProperty='value'
labelProperty='label'
childrenProperty='children'
valueProperty='included'
defaultExpanded
aria-label='Controlled example'
/>
<div className='min-w-48'>
<label className='block mb-2'>
Mode{' '}
<select value={mode} onChange={e => setMode(e.target.value as SelectionMode)} className='text-black rounded px-1'>
<option value='all'>all</option>
<option value='leaves'>leaves</option>
<option value='topmost'>topmost</option>
</select>
</label>
<div>
Selected IDs: <code data-testid='selected-ids'>{JSON.stringify(selectedIDs)}</code>
</div>
<button className='mt-3 border rounded px-2 py-1' onClick={() => setData(exampleData)}>
Reset
</button>
</div>
</div>
);
}

If you pass data but don't update it from onChange, checking a node has no visible effect, the same as a controlled <input> without an onChange handler. This makes it easy to reject or adjust a change before applying it.

What onChange receives​

onChange is called with a new tree every time a selection changes. TreeSelect never modifies the data you pass in:

  • Nodes that changed, and their ancestors, are new objects.
  • Every other node is the same object as before, so unchanged subtrees can be compared with ===.

Treat the result as immutable. If you need to change it, copy the nodes you modify, as you would with any React state.

A single root node​

If your tree has one root, pass the node itself instead of wrapping it in an array. onChange reports a single node too, so the state can stay a single node:

const [root, setRoot] = useState<Category>(rootCategory);

<TreeSelect
data={root}
onChange={setRoot}
uniqueIDProperty='value'
labelProperty='label'
childrenProperty='children'
valueProperty='included'
/>

Loading data later​

data can start as undefined (for example while a request is in flight). TreeSelect renders nothing until it receives data.

Expansion state​

Which nodes are expanded is managed by TreeSelect in both modes. defaultExpanded sets whether nodes start expanded, and changing it later expands or collapses every node.