Aller au contenu principal

Tree

Le composant Tree permet d'afficher une arborescence dynamique et hiérarchique à partir d'un tableau d'éléments. Il supporte le chargement synchrone et asynchrone des données, idéal pour :

  • Naviguer dans des structures hiérarchiques complexes
  • Construire des systèmes de pages imbriquées
  • Afficher une organisation de wiki ou de documentation
  • Gérer des arbres de fichiers ou de catégories
  • Implémenter des menus hiérarchiques interactifs

Le composant offre une expérience utilisateur fluide avec support du chargement à la demande, des actions personnalisées et de la mise à jour dynamique des données.

Utilisation basique

Affiche une arborescence simple avec des données statiques passées via le prop data. Les enfants peuvent être définis directement dans la structure avec la propriété children.

import { Tree } from '@/components/ui/tree';

const mockData = [
{
id: 'foo',
label: 'Foo',
},
{
id: 'bar',
label: 'Bar',
children: [
{
id: 'nested_bar',
label: 'Nested bar',
},
],
},
];

export default function Component() {
return <Tree data={mockData} />
}

Utilisation asynchrone

Charge les enfants à la demande lors de l'expansion d'un élément. Utilisez le prop async avec une fonction onLoad pour récupérer dynamiquement les données depuis une API ou une autre source. Idéal pour les arbres volumineux ou les données en temps réel.

import { Tree } from '@/components/ui/tree';

const mockData = [
{
id: 'foo',
label: 'Foo',
},
{
id: 'bar',
label: 'Bar',
hasChild: true,
},
];

export default function Component() {
/**
* Charge les enfants d'un élément depuis une API (ou une autre source de données)
* @param id - Identifiant de l'élément parent
* @returns Tableau des enfants
*/
async function handleLoad(id) {
try {
const remoteData = await fetch('...');

// Exemple de structure de données à retourner :
// [
// { id: 'nested_foo', label: 'Nested foo' },
// { id: 'nested_bar', label: 'Nested bar', hasChild: true },
// ]
return remoteData;
} catch (error) {
// Si vous souhaitez laisser la possibilité de retenter une chargement
return undefined;

// Si vous ne souhaitez pas laisser la possibilité de retenter une chargement
return [];
}
};

return <Tree data={mockData} async onLoad={handleLoad} />
}

Mise à jour d'un élément racine

Modifiez les éléments au niveau racine en mettant à jour l'état baseData. Ces changements affectent directement le rendu du composant.

import { Tree } from '@/components/ui/tree';
import { useState } from 'react';

const mockData = [
{
id: 'foo',
label: 'Foo',
},
{
id: 'bar',
label: 'Bar',
hasChild: true,
},
];

export default function Component() {
const [baseData, setBaseData] = useState(mockData);

// Pour mettre à jour les éléments racine, il suffit de mettre à jour les données passées au prop data
function handleRootUpdate() {
setBaseData((prevData) => {
return [...]
})
};

return (
<>
<button onClick={handleRootUpdate}>Mise à jour du groupe `nested_bar` (si chargé)</button>
<Tree async data={baseData} onLoad={...} />
</>
)
}

Mise à jour d'un enfant déjà chargé

Modifiez les propriétés d'un élément enfant spécifique en utilisant updateItem. Cette fonction permet une mise à jour directe ou basée sur les données existantes, idéale pour mettre à jour un seul élément sans recharger toute la branche.

import { Tree, useAsyncTreeController } from '@/components/ui/tree';
import { useState } from 'react';

const mockData = [
{
id: 'foo',
label: 'Foo',
},
{
id: 'bar',
label: 'Bar',
hasChild: true,
},
];

export default function Component() {
const [baseData, setBaseData] = useState(mockData);
const { controller, updateItem } = useAsyncTreeController();

/**
* Mise à jour d'un enfant
* - Première approche : mise à jour directe
* - Deuxième approche : mise à jour basée sur la valeur précédente
*/
function handleChildUpdate() {
// Mise à jour directe avec une nouvelle valeur
updateItem('nested_foo', {
label: 'Nested foo (updated)',
hasChild: false,
});

// Mise à jour basée sur l'ancienne valeur
updateItem('nested_foo', (item) => {
return {
...item,
label: 'Nested foo (updated)',
hasChild: false,
};
});
};

return (
<>
<button onClick={handleChildUpdate}>Mise à jour de l'enfant `nested_foo` (si chargé)</button>
<Tree async data={baseData} controller={controller} onLoad={...} />
</>
)
}

Mise à jour d'un groupe d'enfants

Remplacez ou modifiez tous les enfants d'un élément parent en utilisant updateGroup. Cette approche est utile pour ajouter, supprimer ou réorganiser plusieurs enfants en une seule opération.

import { Tree, useAsyncTreeController } from '@/components/ui/tree';
import { useState } from 'react';

const mockData = [
{
id: 'foo',
label: 'Foo',
},
{
id: 'bar',
label: 'Bar',
hasChild: true,
},
];

export default function Component() {
const [baseData, setBaseData] = useState(mockData);
const { controller, updateGroup } = useAsyncTreeController();

/**
* Mise à jour d'un groupe d'enfants
* - Première approche : mise à jour directe
* - Deuxième approche : mise à jour basée sur la valeur précédente
*/
function handleGroupUpdate() {
// Mise à jour directe avec une nouvelle valeur
updateGroup('nested_bar', [
{
id: "double_nested_foo",
label: 'Double nested foo (updated)',
hasChild: false,
},
{
id: "double_nested_bar",
label: 'Double nested bar (updated)',
hasChild: false,
},
]);

// Mise à jour basée sur l'ancienne valeur
updateGroup('nested_bar', (items) => {
return [
...items,
{
id: "double_nested_bar",
label: 'Double nested bar (updated)',
hasChild: false,
},
];
});
};

return (
<>
<button onClick={handleGroupUpdate}>Mise à jour du groupe `nested_bar` (si chargé)</button>
<Tree async data={baseData} controller={controller} onLoad={^...} />
</>
)
}

Référence API

PropTypeDefaultDescription
dataArray<TreeItem<TData>>-Tableau des éléments de l'arborescence. En mode asynchrone, seul le premier niveau est obligatoire
gapnumber4Espacement vertical (en pixels) entre les éléments de l'arborescence
offsetnumber8Indentation (en pixels) appliquée à chaque niveau imbriqué
borderedbooleanfalseAffiche des lignes de connexion verticales pour améliorer la lisibilité des niveaux d'imbrication
disabledbooleanfalseDésactive l'arborescence et applique un style visuel de composant inactif
actions((id: string) => ReactNode) | Partial<{ group: (id: string) => ReactNode; item: (id: string) => ReactNode }>-Configuration des actions personnalisées affichées à côté de chaque élément
asyncbooleanfalseActive le chargement asynchrone des données. Les enfants d'un groupe sont chargés à la première expansion
onLoad(id: string) => Promise<Array<TreeItem<TData>> | undefined>-Fonction de récupération des données du groupe cliqué
controller{ setAsyncItems: RefObject<Dispatch<SetStateAction<Record<string, Array<TreeItem<TData>>>>> | null> }-Instance du contrôleur pour gérer l'état et les interactions de l'arborescence

Typescript

TreeProps

interface TreePropsBase<TData = any> {
data: Array<TreeItem<TData>>;
gap?: number;
offset?: number;
bordered?: boolean;
disabled?: boolean;
actions?: ((id: string) => ReactNode) | Partial<{ group: (id: string) => ReactNode; item: (id: string) => ReactNode }>;
async?: never;
onLoad?: never;
controller?: never;
}

interface TreePropsAsync<TData = any> extends Omit<TreePropsBase<TData>, 'async' | 'onLoad' | 'controller'> {
async: true;
onLoad: (id: string) => Promise<Array<TreeItem<TData>> | undefined>;
controller?: AsyncTreeController<TData>['controller'];
}

export type TreeProps<TData = any> = TreePropsBase<TData> | TreePropsAsync<TData>;

TreeItem

export interface TreeItem<TData = any> {
id: string;
label: ReactNode;
icon?: ((props: LucideProps) => ReturnType<typeof Icon>) | ForwardRefExoticComponent<Omit<LucideProps, 'ref'>>;
hasChild?: boolean;
children?: Array<TreeItem<TData>>;
actions?: (id: string) => ReactNode;
disabled?: boolean;
active?: boolean;
data?: TData;
}

useAsyncTreeController

export interface AsyncTreeController<TData = any> {
controller: {
setAsyncItems: RefObject<Dispatch<SetStateAction<Record<string, Array<TreeItem<TData>>>>> | null>;
};
updateItem: (id: string, value: TreeItem<TData> | ((item: TreeItem<TData>) => TreeItem<TData>)) => void;
updateGroup: (id: string, value: Array<TreeItem<TData>> | ((group: Array<TreeItem<TData>>) => Array<TreeItem<TData>>)) => void;
}