React Tree

React Tree

·

12 min read

React Tree is used to display hierarchical data.

Setup

Refer to PrimeReact setup documentation for download and installation steps for your environment.

Import

import { Tree } from 'primereact/tree';

Getting Started

Tree component requires an array of TreeNode objects as its value.

<Tree value={data} />
const data: [
    {
        "key": "0",
        "label": "Documents",
        "data": "Documents Folder",
        "icon": "pi pi-fw pi-inbox",
        "children": [{
            "key": "0-0",
            "label": "Work",
            "data": "Work Folder",
            "icon": "pi pi-fw pi-cog",
            "children": [{ "key": "0-0-0", "label": "Expenses.doc", "icon": "pi pi-fw pi-file", "data": "Expenses Document" }, { "key": "0-0-1", "label": "Resume.doc", "icon": "pi pi-fw pi-file", "data": "Resume Document" }]
        },
        {
            "key": "0-1",
            "label": "Home",
            "data": "Home Folder",
            "icon": "pi pi-fw pi-home",
            "children": [{ "key": "0-1-0", "label": "Invoices.txt", "icon": "pi pi-fw pi-file", "data": "Invoices for this month" }]
        }]
    },
    {
        "key": "1",
        "label": "Events",
        "data": "Events Folder",
        "icon": "pi pi-fw pi-calendar",
        "children": [
            { "key": "1-0", "label": "Meeting", "icon": "pi pi-fw pi-calendar-plus", "data": "Meeting" },
            { "key": "1-1", "label": "Product Launch", "icon": "pi pi-fw pi-calendar-plus", "data": "Product Launch" },
            { "key": "1-2", "label": "Report Review", "icon": "pi pi-fw pi-calendar-plus", "data": "Report Review" }]
    },
    {
        "key": "2",
        "label": "Movies",
        "data": "Movies Folder",
        "icon": "pi pi-fw pi-star",
        "children": [{
            "key": "2-0",
            "icon": "pi pi-fw pi-star",
            "label": "Al Pacino",
            "data": "Pacino Movies",
            "children": [{ "key": "2-0-0", "label": "Scarface", "icon": "pi pi-fw pi-video", "data": "Scarface Movie" }, { "key": "2-0-1", "label": "Serpico", "icon": "pi pi-fw pi-video", "data": "Serpico Movie" }]
        },
        {
            "key": "2-1",
            "label": "Robert De Niro",
            "icon": "pi pi-fw pi-star",
            "data": "De Niro Movies",
            "children": [{ "key": "2-1-0", "label": "Goodfellas", "icon": "pi pi-fw pi-video", "data": "Goodfellas Movie" }, { "key": "2-1-1", "label": "Untouchables", "icon": "pi pi-fw pi-video", "data": "Untouchables Movie" }]
        }]
    }
]

Controlled vs Uncontrolled

Tree expansion state is managed in two ways, in uncontrolled mode only initial expanded state of a node can be defined using expandedKeys property whereas in controlled mode expandedKeys property along with onToggle properties are used for full control over the state. If you need to expand or collapse the state of nodes programmatically then controlled mode should be used. Example below demonstrates both cases;

import React, { Component } from 'react';
import { Tree } from 'primereact/tree';
import { Button } from 'primereact/button';
import { NodeService } from '../service/NodeService';

export const TreeDemo = () => {

    const [nodes, setNodes] = useState(null);
    const [expandedKeys, setExpandedKeys] = useState({});

    useEffect(() => {
        nodeService = new NodeService();
        nodeService.getTreeNodes().then(data => setNodes(data));
    }, [])

    const toggleMovies = () => {
        let expandedKeys = {...expandedKeys};
        if (expandedKeys['2'])
            delete expandedKeys['2'];
        else
            expandedKeys['2'] = true;

        setExpandedKeys(expandedKeys);
    }

    return (
        <div>
            <h3 className="first">Uncontrolled</h5>
            <Tree value={nodes} />

            <h5>Controlled</h5>
            <Button onClick={toggleMovies} label="Toggle Movies" />
            <Tree value={nodes} expandedKeys={expandedKeys}
                onToggle={e => setExpandedKeys(e.value)} style={{marginTop: '.5em'}} />
        </div>
    )
}

Selection

Tree supports single, multiple and checkbox selection modes. Define selectionMode, selectionKeys and onSelectionChange properties to control the selection. In single mode, selectionKeys should be a single value whereas in multiple or checkbox modes an array is required. By default in multiple selection mode, metaKey is necessary to add to existing selections however this can be configured with metaKeySelection property. Note that in touch enabled devices, Tree does not require metaKey.

import React, {Component} from 'react';
import {Tree} from 'primereact/tree';
import {NodeService} from '../service/NodeService';

export const TreeSelectionDemo = () => {

    const [nodes, setNodes] = useState(null);
    const [selectedNodeKey] = useState(null);
    const [selectedNodeKeys1] = useState(null);
    const [selectedNodeKeys2] = useState(null);
    const [selectedNodeKeys3] = useState(null);

    useEffect(() => {
        nodeService = new NodeService();
        nodeService.getTreeNodes().then(data => setNodes(data));
    }, [])

    return (
        <div>
            <h5>Single Selection</h5>
            <Tree value={nodes} selectionMode="single" selectionKeys={selectedNodeKey} onSelectionChange={e => setSelectedNodeKey(e.value)} />

            <h5>Multiple Selection with MetaKey</h5>
            <Tree value={nodes} selectionMode="multiple" selectionKeys={selectedNodeKeys1} onSelectionChange={e => setSelectedNodeKeys1(e.value)} />

            <h5>Multiple Selection without MetaKey</h5>
            <Tree value={nodes} selectionMode="multiple" metaKeySelection={false} selectionKeys={selectedNodeKeys2} onSelectionChange={e => setSelectedNodeKeys2(e.value)} />

            <h5>Checkbox Selection</h5>
            <Tree value={nodes} selectionMode="checkbox" selectionKeys={selectedNodeKeys3} onSelectionChange={e => setSelectedNodeKeys3(e.value)} />
        </div>
    )
}

Lazy

Lazy loading is implemented using the onExpand event by adding children to the expanded node. leaf property should be enabled to indicate the node has children but not yet loaded. Here is a in-memory demo that loads generated nodes on expand event to imitate a remote call with a timeout. Notice the usage of loading property as well to give users a feedback about the loading process.

import React, {Component} from 'react';
import {Tree} from 'primereact/tree';
import {NodeService} from '../service/NodeService';

export const TreeLazyDemo = () => {

    const [nodes, setNodes] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        nodeService = new NodeService();
        setTimeout(() => {
            nodeService.getTreeNodes().then(data => {
                setNodes(data);
                setLoading(false);
            });
        }, 2000);
    }, [])

    const loadOnExpand = (event) => {
        if (!event.node.children) {
            setLoading(true)

            setTimeout(() => {
                let node = {...event.node};
                node.children = [];

                for (let i = 0; i < 3; i++) {
                    node.children.push({
                        key: node.key + '-' + i,
                        label: 'Lazy ' + node.label + '-' + i
                    });
                }

                let value = [...nodes];
                value[parseInt(event.node.key, 10)] = node;
                setNodes(value);
                setLoading(false);
            }, 500);
        }
    }

    return (
        <Tree value={nodes} onExpand={loadOnExpand} loading={loading} />
    )
}

Templating

label property of a node is used to display as the content by default. Templating is supported as well with the nodeTemplate callback that gets the node instance and returns JSX. Example below is a sample tree based navigation of React docs.

import React, { Component } from 'react';
import { Tree } from 'primereact/tree';

export const TreeTemplatingDemo = () => {

    const [nodes, setNodes] = useState(createNavigation());

    const createNavigation = () => {
        return [
            {
                label: 'Insallation',
                children: [
                    {label: 'Getting Started', url:'https://reactjs.org/docs/getting-started.html'},
                    {label: 'Add React', url: 'https://reactjs.org/docs/add-react-to-a-website.html'},
                    {label: 'Create an App', url:'https://reactjs.org/docs/create-a-new-react-app.html'},
                    {label: 'CDN Links', url: 'https://reactjs.org/docs/cdn-links.html'}
                ]
            },
            {
                label: 'Main Concepts',
                children: [
                    {label: 'Hello World', url: 'https://reactjs.org/docs/hello-world.html'},
                    {label: 'Introducing JSX', url: 'https://reactjs.org/docs/introducing-jsx.html'},
                    {label: 'Rendering Elements', url: 'https://reactjs.org/docs/rendering-elements.html'},
                    {label: 'Components and Props', url: 'https://reactjs.org/docs/components-and-props.html'},
                    {label: 'State and LifeCycle', url: 'https://reactjs.org/docs/state-and-lifecycle.html'},
                    {label: 'Handling Events', url: 'https://reactjs.org/docs/handling-events.html'}
                ]
            }
        ];
    }

    const nodeTemplate = (node) => {
        if (node.url) {
            return (
                <a href={node.url}>{node.label}</a>
            )
        }
        else {
            return (
                <b>{node.label}</b>
            )
        }
    }

    return (
        <Tree value={nodes} nodeTemplate={nodeTemplate} />
    )
}

DragDrop

Tree nodes can be reordered using dragdrop by setting dragdropScope property to a unique variable and updating the new value at onDragDrop callback. The value of the dragdropScope must be unique to provide intervention from other draggable elements on the page.

import React, {Component} from 'react';
import {Tree} from 'primereact/tree';
import {NodeService} from '../service/NodeService';

export const TreeDragDropDemo = () => {

    const [nodes, setNodes] = useState(null);

    useEffect(() => {
        nodeService = new NodeService();
        nodeService.getTreeNodes().then(data => setNodes(data));
    }, [])

    return (
        <div>
            <Tree value={nodes} dragdropScope="demo" onDragDrop={event => setNodes(event.value)} />
        </div>
    )
}

Filtering

Filtering is enabled by setting the filter property to true, by default label property of a node is used to compare against the value in the text field, in order to customize which field(s) should be used during search define filterBy property.

In addition filterMode specifies the filtering strategy. In lenient mode when the query matches a node, children of the node are not searched further as all descendants of the node are included. On the other hand, in strict mode when the query matches a node, filtering continues on all descendants.

<Tree value={nodes} filter />

<Tree value={nodes} filter filterBy="data.name,data.age" />

<Tree value={nodes} filter filterMode="strict" />

ContextMenu

One or more ContextMenu instances can be attached to nodes. Similar to selection, separate contextMenuSelectionKey and onContextMenuSelectionChange properties are necesary to manage the selected node with right click. In addition, a context menu can either be displayed at onContextMenu event. Since this event also passes the node instance, you may choose to display a different context menu for a particular node.

import React, { Component } from 'react';
import {Tree} from 'primereact/tree'
import {ContextMenu} from 'primereact/contextmenu';
import {Toast} from 'primereact/toast';
import {NodeService} from '../service/NodeService';

export const TreeContextMenuDemo = () => {

    const [nodes, setNodes] = useState(null);
    const [expandedKeys, setExpandedKeys] = useState({});
    const [selectedNodeKey, setSelectedNodeKey] = useState(null);
    const toast = useRef(null);
    const cm = useRef(null);
    const menu = [
        {
            label: 'View Key',
            icon: 'pi pi-search',
            command: () => {
                toast.current.show({severity: 'success', summary: 'Node Key', detail: selectedNodeKey});
            }
        },
        {
            label: 'Toggle',
            icon: 'pi pi-cog',
            command: () => {
                let expandedKeys = {...expandedKeys};
                if (expandedKeys[selectedNodeKey])
                    delete expandedKeys[selectedNodeKey];
                else
                    expandedKeys[selectedNodeKey] = true;
                setExpandedKeys(expandedKeys);
            }
        }
    ];

    useEffect(() => {
        nodeService = new NodeService();
        nodeService.getTreeNodes().then(data => setNodes(data));
    }, [])

    return (
        <div>
            <Toast ref={toast} />

            <ContextMenu model={menu} ref={cm} />

            <Tree value={nodes} expandedKeys={expandedKeys} onToggle={e => setExpandedkeys(e.value)}
                onContextMenuSelectionChange={event => setSelectedNodeKey(event.value)}
                onContextMenu={event => cm.current.show(event.originalEvent)} />
        </div>
    )
}

Theming

Tree supports various themes featuring Material, Bootstrap, Fluent as well as your own custom themes via the Designer tool.

Resources

Visit the PrimeReact Tree showcase for demos and documentation.