JavaScript is required

Lazy Load Tree Children on Expand

A top-down tree demo that lazy-loads child nodes when a collapsed branch expands. It appends the returned graph fragment into the live relation-graph instance, reruns layout, and shows both page-level and node-level loading feedback.

Expand a Tree with Lazy-Loaded Child Nodes

What This Example Builds

This example builds a top-down tree that grows when the user expands marked nodes. The initial graph is intentionally small, then a collapsed branch requests more descendants on demand and appends them into the existing relation-graph instance instead of replacing the full dataset.

On screen, the user sees beige rectangular nodes, orthogonal connectors, bottom-positioned expand controls, and a floating white utility window above the canvas. During async expansion, the page shows a loading mask and the active node also shows its own spinner badge, so the branch being resolved is easy to identify.

The most useful idea in this demo is not just lazy loading by itself. It is the combination of one-time branch loading, recursive repeatability on newly appended descendants, and explicit relayout after the new fragment is inserted.

How the Data Is Organized

The graph uses RGJsonData with a rootId, a flat nodes array, and a flat lines array. The seed dataset is created inside initializeGraph(), and one node is pre-marked with expanded: false, expandHolderPosition: 'bottom', and data.isNeedLoadDataFromRemoteServer: true so that expansion becomes the trigger for loading.

There is no heavy preprocessing before the initial setJsonData(...) call. The example constructs the initial tree inline, loads it into the graph, then centers and fits the viewport. The async branch loader also returns plain nodes and lines, which means the same pattern could be backed by API responses, database lookups, folder trees, dependency trees, or any other hierarchical source that can be expressed as graph fragments.

Before appended nodes are inserted, the example copies the expanded node’s x and y coordinates onto every new node. That small step makes the subsequent doLayout() feel like an expansion from the clicked branch rather than a full rebuild from an unrelated position.

How relation-graph Is Used

The example is mounted inside RGProvider, and RGHooks.useGraphInstance() is the main control surface. The graph options configure a tree layout with from: 'top', treeNodeGapV: 120, rectangular nodes, orthogonal lines, top-bottom junction points, bottom expand holders, and automatic relayout for ordinary expand or collapse changes.

The graph instance API drives the runtime behavior. setJsonData(...) loads the seed tree, moveToCenter() and zoomToFit() prepare the first view, updateOptions(...) synchronizes the relayout switch with React state, updateNodeData(...) marks loading state on the active node, addNodes(...) and addLines(...) append new graph fragments, sleep(...) creates a short pause before layout, and doLayout() recomputes the tree after async insertion.

Custom rendering is focused and functional. RGSlotOnNode renders the node text and conditionally adds a spinner bubble under the current loading node. The surrounding DraggableWindow helper adds a floating explanation area plus a settings panel that can change wheel behavior, change canvas drag behavior, and export an image through prepareForImageGeneration() and restoreAfterImageGeneration().

The local SCSS file is minimal and does not appear to define the main visual identity of the rendered graph. Most of the visible styling in this example comes from graph options, utility classes, and the custom node slot.

Key Interactions

  • Expanding a node with data.isNeedLoadDataFromRemoteServer triggers the async child-generation flow.
  • The same node will not append duplicates on later expands because childrenLoaded is written back into node data.
  • Newly appended descendants can continue the pattern because one returned child is also marked as collapsed and remote-loadable.
  • A floating switch updates reLayoutWhenExpandedOrCollapsed for normal expand and collapse behavior, while the helper text makes clear that dynamically appended content still forces a relayout.
  • The settings panel in the floating window can change wheel mode, change canvas drag mode, and download a graph image.
  • The utility window itself can be dragged and minimized, so the explanation layer does not block the graph surface.

Key Code Fragments

This seed dataset shows how the initial tree embeds a collapsed branch that is eligible for lazy loading.

const myJsonData: RGJsonData = {
    rootId: 'a',
    nodes: [
        { id: 'a', text: 'a' },
        { id: 'b', text: 'b' },
        { id: 'b1', text: 'b1' },
        { id: 'b2', text: 'b2' },
        { id: 'b2-1', text: 'b2-1', expandHolderPosition: 'bottom', expanded: false, data: { isNeedLoadDataFromRemoteServer: true } },
        { id: 'b2-2', text: 'b2-2' }
    ],

This guard logic proves that only marked nodes load children and that each branch appends its remote-style data only once.

if (!node.data.isNeedLoadDataFromRemoteServer) {
    return;
}
if (node.data.childrenLoaded) {
    return;
}
setLoading(true);
// Effect is equivalent to: node.data.childrenLoaded = true; updateNodeData method updates data to support reactivity
graphInstance.updateNodeData(node, {
    childrenLoaded: true, // Mark node as: dynamic child nodes loaded
    myLoading: true // Add style to node: loading
});

This append sequence shows the core runtime pattern: place new nodes near the expanded parent, append them, wait briefly, and rerun layout.

newJsonData.nodes.forEach(newNode => {
    newNode.x = node.x; // JsonNode object can directly modify properties before being added to graph, no need to use graphInstance.updateNode method
    newNode.y = node.y;
});
// Use API to append data
graphInstance.addNodes(newJsonData.nodes);
graphInstance.addLines(newJsonData.lines);
await graphInstance.sleep(350);
await graphInstance.doLayout();
setLoading(false);

This node slot turns loading state into an inline visual cue on the branch that is currently expanding.

<RGSlotOnNode>
    {({ node }: RGNodeSlotProps) => (
        <div className="h-full w-full flex place-items-center justify-center">
            {node.text}
            {
                node.data.myLoading && <div className="absolute rounded-full bottom-[-23px] w-[20px] h-[20px] bg-white border border-gray-300 flex place-items-center justify-center">
                    <Loader2Icon className="animate-spin" size={16} />
                </div>
            }
        </div>
    )}
</RGSlotOnNode>

This runtime toggle shows that expand or collapse relayout is treated as a live option rather than a fixed boot-time setting.

const setRelayoutWhenExpandedOrCollapsed = (newValue: boolean) => {
    // 3.x recommends using updateOptions to modify config
    graphInstance.updateOptions({ reLayoutWhenExpandedOrCollapsed: newValue });
    setRelayout(newValue);
};

What Makes This Example Distinct

Compared with nearby lazy-growth examples such as expand-button, this demo is more specifically about recursive top-down tree growth than about simply exposing expand controls on placeholder leaves. It marks one newly appended child as another future lazy-load target, so the reveal pattern can continue deeper instead of ending after the first expansion.

Compared with investment and investment-penetration, this example keeps the content generic and the node visuals relatively plain. That makes the reusable lesson clearer: how to intercept expansion, guard one-time async loads, append graph fragments with addNodes(...) and addLines(...), and present both global and branch-level loading feedback.

Within the examples that also reuse the floating helper window, this one puts unusual emphasis on branch-specific status feedback and runtime relayout preference. The distinctive combination is a warm top-down orthogonal tree, bottom expand holders, explicit async append-and-relayout flow, a page-level loading mask, and an inline spinner rendered directly inside the active node slot.

Where Else This Pattern Applies

This pattern transfers well to any hierarchy that is too large or too expensive to load upfront. A few common examples are organization charts that fetch sub-teams on demand, file explorers that request folder contents only when opened, dependency trees that reveal downstream modules incrementally, and investigation graphs that expand one causal branch at a time.

It is also useful when users need clear feedback during async expansion. The combination of graph-level loading state and node-level loading state works well for knowledge exploration tools, approval trees, infrastructure ownership maps, and other interfaces where users need to know which branch is still resolving before they continue navigating.