Skip to content
+

Rich Tree View - Lazy loading

Lazy load the data from your Tree View.

Basic usage

To dynamically load data from the server, including lazy-loading of children, you must create a data source and pass the dataSource prop to RichTreeView.

The data source also requires the getChildrenCount() attribute to handle tree data. getChildrenCount() returns the number of children for the item. If the children count is not available, but there are children present, it returns -1.

The items prop serves as the initial state.

MUI X Missing license key
import {
  randomInt,
  randomName,
  randomId,
  randomBoolean,
} from '@mui/x-data-grid-generator';
import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro';
import type { ItemType } from './items';
import { initialItems } from './items';

const fetchData = async (): Promise<ItemType[]> => {
  const length: number = randomInt(2, 10);
  const rows = Array.from({ length }, () => ({
    id: randomId(),
    label: randomName({}, {}),
    ...(randomBoolean() ? { childrenCount: length } : {}),
  }));

  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(rows);
    }, 1000);
  });
};

export default function LazyLoadingInitialState() {
  return (
    <div style={{ width: 300, height: 240 }}>
      <RichTreeViewPro
        items={initialItems}
        dataSource={{
          getChildrenCount: (item) => item?.childrenCount as number,
          getTreeItems: fetchData,
        }}
      />
    </div>
  );
}

If you want to dynamically load all items of RichTreeView, you can pass an empty array to the items prop, and the getTreeItems() method will be called on the first render.

Loading latency: 1000 (ms)

MUI X Missing license key

Using react-query

The following demo uses fetchQuery from react-query to load data.

MUI X Missing license key
import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro';
import {
  randomInt,
  randomName,
  randomId,
  randomBoolean,
} from '@mui/x-data-grid-generator';
import {
  QueryClient,
  QueryClientProvider,
  useQueryClient,
} from '@tanstack/react-query';

const queryClient = new QueryClient();

type ItemType = {
  id: string;
  label: string;
  childrenCount?: number;
  children?: ItemType[];
};

const items: ItemType[] = [];

const fetchData = async (_parentId?: string): Promise<ItemType[]> => {
  const length: number = randomInt(5, 10);
  const rows = Array.from({ length }, () => ({
    id: randomId(),
    label: randomName({}, {}),
    ...(randomBoolean() ? { childrenCount: length } : {}),
  }));

  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(rows);
    }, 1000);
  });
};

export function FetchChildren() {
  const myQueryClient = useQueryClient();

  const fetchTreeItems = async (parentId?: string) => {
    const queryKey = parentId ? ['treeItems', parentId] : ['treeItems', 'root'];
    const data = await myQueryClient.query({
      queryKey,
      queryFn: () => fetchData(parentId),
    });
    return data;
  };

  return (
    <div style={{ width: 300, height: 240 }}>
      <RichTreeViewPro
        items={items}
        dataSource={{
          getChildrenCount: (item) => item?.childrenCount as number,
          getTreeItems: fetchTreeItems,
        }}
      />
    </div>
  );
}

export default function FetchingWithReactQuery() {
  return (
    <QueryClientProvider client={queryClient}>
      <FetchChildren />
    </QueryClientProvider>
  );
}

Loading indicators

While getTreeItems() fetches the root items, the tree displays the same loading rows as the loading prop. Use the itemsCount value in slotProps.loading to control the number of rows.

While an item fetches its children, the loading UI depends on the DOM structure:

  • With domStructure="nested" and disableVirtualization, the item opens and displays loading rows in place of its children. The number of rows matches getChildrenCount() when the count is known.
  • With the default flat structure, a circular progress indicator replaces the expansion icon.

You can customize the loading rows with the same slots and classes as the loading prop.

    MUI X Missing license key
    import * as React from 'react';
    import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro';
    
    type ItemType = {
      id: string;
      label: string;
      childrenCount: number;
    };
    
    const getTreeItems = async (parentId?: string): Promise<ItemType[]> => {
      const depth = parentId == null ? 0 : parentId.split('.').length;
      const items = Array.from({ length: 4 }, (_, index) => {
        const id = parentId == null ? `${index + 1}` : `${parentId}.${index + 1}`;
        return {
          id,
          label: `Item ${id}`,
          // Stop nesting after the third level.
          childrenCount: depth < 2 ? 4 : 0,
        };
      });
    
      return new Promise((resolve) => {
        setTimeout(() => resolve(items), 1500);
      });
    };
    
    export default function ChildrenLoadingRows() {
      return (
        <div style={{ width: 300 }}>
          <RichTreeViewPro
            items={[]}
            domStructure="nested"
            disableVirtualization
            dataSource={{
              getChildrenCount: (item) => item?.childrenCount as number,
              getTreeItems,
            }}
          />
        </div>
      );
    }
    

    Customized loading UI

    The demo below combines the customization options and adds transitions:

    • The loading slot replaces the loading rows. It renders while the root items load and while an item fetches its children. Each custom row is wrapped in the TreeItemLoader component, which keeps the role, aria attributes, indentation, and height of a tree item.
    • The loadingIcon slot of TreeItem replaces the circular progress indicator of an item whose children load.
    • The groupTransition slot of TreeItem animates the expansion with a spring.
    • The loading rows and the items share a staggered entrance animation. This makes the transition from the loading state to the loaded items read as one continuous motion.
      MUI X Missing license key
      import * as React from 'react';
      import Box from '@mui/material/Box';
      import Collapse from '@mui/material/Collapse';
      import Skeleton from '@mui/material/Skeleton';
      import type { TransitionProps } from '@mui/material/transitions';
      import { useSpring, animated } from '@react-spring/web';
      import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro';
      import type { TreeItemProps } from '@mui/x-tree-view/TreeItem';
      import { TreeItem, treeItemClasses } from '@mui/x-tree-view/TreeItem';
      import {
        TreeItemLoader,
        treeItemLoaderClasses,
      } from '@mui/x-tree-view/TreeItemLoader';
      
      type ItemType = {
        id: string;
        label: string;
        childrenCount: number;
      };
      
      const sleep = (ms: number) =>
        new Promise<void>((resolve) => {
          setTimeout(resolve, ms);
        });
      
      async function getTreeItems(parentId?: string): Promise<ItemType[]> {
        await sleep(parentId == null ? 1200 : 1800);
      
        const depth = parentId == null ? 0 : parentId.split('-').length;
        return Array.from({ length: 4 }, (_, index) => {
          const id = parentId == null ? `${index + 1}` : `${parentId}-${index + 1}`;
          // The first two items of the first two levels are expandable.
          const expandable = depth < 2 && index < 2;
          return {
            id,
            label: `${expandable ? 'Folder' : 'File'} ${id.replace(/-/g, '.')}`,
            childrenCount: expandable ? 4 : 0,
          };
        });
      }
      
      const dataSource = {
        getChildrenCount: (item: ItemType) => item?.childrenCount ?? 0,
        getTreeItems,
      };
      
      // Spring-based expand/collapse of the item groups.
      function SpringGroupTransition(props: TransitionProps) {
        const style = useSpring({
          to: {
            opacity: props.in ? 1 : 0,
            transform: `translate3d(0,${props.in ? 0 : -10}px,0)`,
          },
        });
      
        return (
          <animated.div style={style}>
            <Collapse {...props} />
          </animated.div>
        );
      }
      
      const bouncingDotStyles = {
        display: 'inline-flex',
        gap: '3px',
        alignItems: 'center',
        '& > span': {
          width: 4,
          height: 4,
          borderRadius: '50%',
          backgroundColor: 'primary.main',
          animation: 'bouncingDot 0.9s ease-in-out infinite',
        },
        '& > span:nth-of-type(2)': { animationDelay: '0.15s' },
        '& > span:nth-of-type(3)': { animationDelay: '0.3s' },
        '@keyframes bouncingDot': {
          '0%, 100%': { transform: 'translateY(0)', opacity: 0.5 },
          '50%': { transform: 'translateY(-3px)', opacity: 1 },
        },
      };
      
      // Three bouncing dots replace the circular progress while an item loads its children.
      // The component also receives the `size` and `thickness` props of the default icon,
      // which it ignores.
      function BouncingDotsLoadingIcon(props: { className?: string }) {
        return (
          <Box component="span" className={props.className} sx={bouncingDotStyles}>
            <span />
            <span />
            <span />
          </Box>
        );
      }
      
      const CustomTreeItem = React.forwardRef(function CustomTreeItem(
        props: TreeItemProps,
        ref: React.Ref<HTMLLIElement>,
      ) {
        return (
          <TreeItem
            {...props}
            ref={ref}
            slots={{
              loadingIcon: BouncingDotsLoadingIcon,
              groupTransition: SpringGroupTransition,
            }}
          />
        );
      });
      
      // Rendered instead of the default loading rows, for the whole tree and for the
      // children of a lazily loading item.
      function CustomLoading(props: { itemsCount?: number }) {
        const { itemsCount = 0 } = props;
      
        return (
          <React.Fragment>
            {Array.from({ length: itemsCount }, (_, index) => (
              <TreeItemLoader key={index} style={{ animationDelay: `${index * 90}ms` }}>
                <Skeleton
                  variant="circular"
                  width={18}
                  height={18}
                  sx={{ flexShrink: 0 }}
                />
                <Skeleton
                  variant="rounded"
                  height={12}
                  sx={{ flexGrow: 1, maxWidth: `${75 - index * 12}%` }}
                />
              </TreeItemLoader>
            ))}
          </React.Fragment>
        );
      }
      
      // Number of sibling rows with a staggered entrance delay.
      const STAGGERED_ROWS_COUNT = 8;
      
      // Staggered entrance of sibling items, keyed by `nth-of-type` selectors.
      // The loading rows set their delay inline instead.
      const ITEM_STAGGER_DELAYS: Record<string, { animationDelay: string }> =
        Object.fromEntries(
          Array.from({ length: STAGGERED_ROWS_COUNT }, (_, index) => [
            `& .${treeItemClasses.root}:nth-of-type(${index + 1}) > .${treeItemClasses.content}`,
            { animationDelay: `${index * 60}ms` },
          ]),
        );
      
      const transitionStyles = {
        // The loading rows and the items share the same springy entrance,
        // so the swap between them reads as one continuous motion.
        [`& .${treeItemLoaderClasses.root}, & .${treeItemClasses.content}`]: {
          borderRadius: 1,
          transformOrigin: 'left center',
          animation: 'popIn 420ms cubic-bezier(0.34, 1.56, 0.64, 1) backwards',
        },
        ...ITEM_STAGGER_DELAYS,
        '@keyframes popIn': {
          from: { opacity: 0, transform: 'scale(0.85) translateX(-8px)' },
          to: { opacity: 1, transform: 'none' },
        },
      };
      
      export default function CustomizedLazyLoading() {
        return (
          <Box sx={{ width: 300, minHeight: 200 }}>
            <RichTreeViewPro
              items={[]}
              domStructure="nested"
              disableVirtualization
              itemHeight={32}
              slots={{ item: CustomTreeItem, loading: CustomLoading }}
              slotProps={{ loading: { itemsCount: 4 } }}
              dataSource={dataSource}
              sx={transitionStyles}
            />
          </Box>
        );
      }
      

      Data caching

      Custom cache

      To provide a custom cache, use the dataSourceCache prop, which may be created from scratch or based on a third-party cache library. This prop accepts a generic interface of type DataSourceCache.

      The following demo uses QueryClient from react-query as a data source cache.

      MUI X Missing license key
      import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro';
      import {
        randomInt,
        randomName,
        randomId,
        randomBoolean,
      } from '@mui/x-data-grid-generator';
      import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
      import type { DataSourceCache } from '@mui/x-tree-view/utils';
      
      const queryClient = new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 1000 * 60 * 60,
          },
        },
      });
      
      const cache: DataSourceCache = {
        set: (key: string, value) => {
          queryClient.setQueryData([key], value);
        },
        get: (key: string) => {
          return queryClient.getQueryData([key]);
        },
        clear: () => {
          queryClient.clear();
        },
      };
      
      type ItemType = {
        id: string;
        label: string;
        childrenCount?: number;
        children?: ItemType[];
      };
      const fetchData = async (_parentId?: string): Promise<ItemType[]> => {
        const length: number = randomInt(5, 10);
        const rows = Array.from({ length }, () => ({
          id: randomId(),
          label: randomName({}, {}),
          ...(randomBoolean() ? { childrenCount: length } : {}),
        }));
      
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve(rows);
          }, 1000);
        });
      };
      
      export function FetchChildren() {
        return (
          <div style={{ width: 300, height: 240 }}>
            <RichTreeViewPro
              items={[]}
              dataSource={{
                getChildrenCount: (item) => item?.childrenCount as number,
                getTreeItems: fetchData,
              }}
              dataSourceCache={cache}
            />
          </div>
        );
      }
      
      export default function ReactQueryCache() {
        return (
          <QueryClientProvider client={queryClient}>
            <FetchChildren />
          </QueryClientProvider>
        );
      }
      

      Customize the cache lifetime

      The DataSourceCacheDefault has a default time to live (ttl) of 5 minutes. To customize it, pass the ttl option in milliseconds to the DataSourceCacheDefault constructor, and then pass it as the dataSourceCache prop.

      MUI X Missing license key
      import { randomInt, randomName, randomId } from '@mui/x-data-grid-generator';
      import { RichTreeViewPro } from '@mui/x-tree-view-pro/RichTreeViewPro';
      import { DataSourceCacheDefault } from '@mui/x-tree-view/utils';
      
      type ItemType = {
        id: string;
        label: string;
        childrenCount?: number;
        children?: ItemType[];
      };
      
      const fetchData = async (): Promise<ItemType[]> => {
        const rows = Array.from({ length: 10 }, () => ({
          id: randomId(),
          label: randomName({}, {}),
          ...(randomInt(0, 1) ? { childrenCount: 10 } : {}),
        }));
      
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve(rows);
          }, 1000);
        });
      };
      
      const lowTTLCache = new DataSourceCacheDefault({ ttl: 1000 * 10 }); // 10 seconds
      
      export default function LowTTLCache() {
        return (
          <div style={{ width: 300, height: 240 }}>
            <RichTreeViewPro
              items={[]}
              dataSource={{
                getChildrenCount: (item) => item?.childrenCount as number,
                getTreeItems: fetchData,
              }}
              dataSourceCache={lowTTLCache}
            />
          </div>
        );
      }
      

      Auto-expand lazy-loaded items

      Use the onItemsLazyLoaded callback to auto-expand items when their children are loaded. This callback is called both when new items are fetched from the server and when items are loaded from the cache.

      In the example below, the server returns some items with their sub-items already included in the response. The onItemsLazyLoaded callback expands any returned item that has inline children. Because those children are pre-cached by the tree view, expanding them requires no additional network request.

      MUI X Missing license key

      Error management

      When the fetch of an item's children fails, the item closes again and displays an error indicator in its icon container.

      MUI X Missing license key

      Lazy loading and label editing

      To store the updated item labels on your server, use the onItemLabelChange() callback function.

      Changes to the label are not automatically updated in the dataSourceCache and must be updated manually. The demo below shows you how to update the cache once a label is changed so the changes are reflected in the tree.

      MUI X Missing license key

      Lazy loading and adding items

      Items added with the addItems() API method are only stored in the internal state of the component and are not added to the dataSourceCache. Collapsing and expanding their parent again replaces them with the response of the server.

      The demo below shows you how to update the cache once an item is added so the changes are reflected in the tree. The children of the selected item are loaded before the new item is added, because the fetch triggered when expanding an item overrides the items added to it.

      MUI X Missing license key

      Imperative API

      To use the apiRef object, you need to initialize it using the useRichTreeViewProApiRef() hook as follows:

      const apiRef = useRichTreeViewProApiRef();
      
      return <RichTreeViewPro apiRef={apiRef} items={ITEMS} />;
      

      When your component first renders, apiRef.current is undefined. After the initial render, apiRef holds methods to interact imperatively with RichTreeView.

      Update the children of an item

      Use the updateItemChildren() API method to fetch the children of an item:

      apiRef.current.updateItemChildren(
        // The id of the item to update the children of
        // null if the entire tree needs to be reloaded
        itemId,
      );
      
      MUI X Missing license key

      API

      See the documentation below for a complete reference to all of the props and classes available to the components mentioned here.