Skip to main content
react-md

Render Recursively

The RenderRecursively component is a low-level rendering component to render recursive data. This is used internally for the Tree and Navigation components, but can be used for other functionality like this website's Table Of Contents or other recursive components.

Simple Example

Active route: /
"use client";

import { Box } from "@react-md/core/box/Box";
import { Button } from "@react-md/core/button/Button";
import { Divider } from "@react-md/core/divider/Divider";
import { List } from "@react-md/core/list/List";
import { useCollapseTransition } from "@react-md/core/transition/useCollapseTransition";
import {
  type DefaultTreeItemNode,
  type TreeData,
} from "@react-md/core/tree/types";
import { type UseStateSetter } from "@react-md/core/types";
import { useToggle } from "@react-md/core/useToggle";
import {
  type RenderRecursiveItemsProps,
  RenderRecursively,
} from "@react-md/core/utils/RenderRecursively";
import { type ReactElement, useState } from "react";

interface Data {
  activeId: string;
  setActiveId: UseStateSetter<string>;
}

export default function SimpleExample(): ReactElement {
  const [activeId, setActiveId] = useState("/");
  return (
    <Box fullWidth disableWrap>
      <Box stacked align="stretch">
        <RenderRecursively
          // this is optional and can be any sort of info
          data={{ activeId, setActiveId }}
          items={Object.values(navItems)}
          render={Render}
        />
      </Box>
      <div>Active route: {activeId}</div>
    </Box>
  );
}

const navItems = {
  "/": {
    href: "/",
    itemId: "/",
    parentId: null,
    children: "Home",
  },
  "/route-1": {
    itemId: "/route-1",
    parentId: null,
    children: "Collapsible",
  },
  "/nested-route-1": {
    itemId: "/nested-route-1",
    parentId: "/route-1",
    children: "Child 1",
  },
  "/nested-route-2": {
    itemId: "/nested-route-2",
    parentId: "/route-2",
    children: "Child 2",
  },
  "/divider-1": {
    itemId: "/divider-1",
    parentId: null,
  },
  "/route-2": {
    itemId: "/route-2",
    parentId: null,
    children: "Route 2",
  },
} satisfies TreeData;

function Render(
  props: RenderRecursiveItemsProps<DefaultTreeItemNode, Data>,
): ReactElement {
  const {
    item,
    parents,
    // this is passed down from the root `RenderRecursively`
    data,
    // this _might_ be defined based on the implementation. it's set to any
    // renderable element
    children: _children,
  } = props;
  if (item.itemId.includes("divider")) {
    return <Divider />;
  }

  if (item.items) {
    return <NestedNavigation {...props} />;
  }

  const prefix = parents.map((parent) => parent.itemId).join("/");
  const currentId = `${prefix}${item.itemId}`;
  // I haven't figured out how to manage the type safety of this yet
  const active = data?.activeId === currentId;
  // this would normally be a link, but to keep the demo simple, it's a button
  return (
    <Button
      theme={active ? "primary" : "clear"}
      themeType={active ? "contained" : "flat"}
      onClick={() => {
        data?.setActiveId(currentId);
      }}
      style={{ justifyContent: "flex-start" }}
    >
      {item.children}
    </Button>
  );
}

function NestedNavigation(
  props: RenderRecursiveItemsProps<DefaultTreeItemNode, Data>,
): ReactElement {
  const {
    item,
    parents: _parents,
    // this is passed down from the root `RenderRecursively`
    data: _data,
    children,
  } = props;
  const { toggle, toggled: collapsed } = useToggle(false);
  const { elementProps } = useCollapseTransition({
    transitionIn: !collapsed,
  });

  return (
    <li>
      <Button onClick={toggle}>{item.children}</Button>
      <List {...elementProps}>{children}</List>
    </li>
  );
}

Press Enter to start editing.