Theme
Version
GitHub PyPI Discord
On this page

Recursion

Use this pattern to draw nested data, such as a file tree, comment threads, or a menu. TreeNode renders one node's label, then renders <c-TreeNode> again for each child.

"""A self-rendering tree, used as a live example in the docs.

TreeNode renders one node's label and then renders itself for each
child, so one component draws the whole nested structure.
"""

from __future__ import annotations

from typing import Any

# Both come from typing_extensions so a NotRequired key is optional
# on Python 3.10 too.
from typing_extensions import NotRequired, TypedDict

from citry import Component

class TreeNodeData(TypedDict):
    label: str
    children: NotRequired[list[TreeNodeData]]

class TreeNode(Component):
    """One node of a tree: its label, then itself for each child.

    The recursion stops on its own: a leaf has no children, so the
    ``c-if`` skips the child list and no further TreeNode is rendered.
    """

    class Kwargs:
        node: TreeNodeData

    class Slots:
        pass

    def template_data(
        self,
        kwargs: Kwargs,
        slots: Slots,
    ) -> dict[str, Any]:
        node = kwargs.node
        children = node.get("children", [])
        has_children = len(children) > 0
        # A down arrow marks a node with children; a dot marks a
        # leaf.
        icon = "▾" if has_children else "•"
        return {
            "label": node["label"],
            "children": children,
            "has_children": has_children,
            "icon": icon,
        }

    template = """
      <div class="tree-node">
        <span class="tree-node__label">
          <span class="tree-node__icon">{{ icon }}</span>
          {{ label }}
        </span>
        <div c-if="has_children" class="tree-node__children">
          <c-TreeNode c-for="child in children" c-node="child" />
        </div>
      </div>
    """

    css = """
      .tree-node {
        font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
        font-size: 0.95rem;
      }
      .tree-node__label {
        display: inline-flex;
        align-items: center;
        gap: 0.4rem;
        padding: 0.15rem 0;
        color: #1f2328;
      }
      .tree-node__icon {
        color: #6e7781;
      }
      .tree-node__children {
        margin-left: 1.1rem;
        padding-left: 0.75rem;
        border-left: 1px solid #d0d7de;
      }
    """

The recursion stops on its own. A leaf has no children, so the c-if on the child list is false and no further TreeNode renders. Your own data needs the same end.

Stop endless recursion

A node that appears again inside its own children never reaches a leaf. Citry stops such a render with a RecursionError once components are nested more than 2,000 levels deep. The message starts with the component that repeats:

Component TreeNode is nested more than 2000 components deep
(TreeNode > TreeNode > ... > TreeNode > TreeNode > TreeNode).

<c-error-fallback> does not catch this error, so the whole render fails. Fix the data so every branch ends in a leaf. If your page really nests deeper than 2,000 components, raise the limit with the max_component_depth setting:

app = Citry(max_component_depth=5_000)