demo D81
Lazy-Load Tree Data Children on Expand in React
Fetching a branch on expand, with an abort when it closes first
tree.loadChildren
This grid fetches a branch of the tree only when a row is expanded, and aborts the request if the branch is closed again first. It suits a hierarchy behind a remote API, loading each level on demand instead of pulling the whole tree up front.
This is the React version. The grid mounts through the createLatticeGrid adapter, which takes its configuration as ordinary props and hands back the live grid through a ref. The grid below is the same one every other tab runs.
The configuration
<link rel="stylesheet" href="https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.min.css">
<div id="grid"></div>
<script type="module">
import React from 'https://esm.sh/react@18';
import { createRoot } from 'https://esm.sh/react-dom@18/client';
import { createGrid } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.esm.min.js';
import createLatticeGrid from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/react.esm.min.js';
const LatticeGrid = createLatticeGrid({ React, createGrid });
const money = { style: 'currency', currency: 'GBP', decimals: 0 };
// Stands in for the server the branch would come from. The delay makes the
// loading state and the abort both visible; the signal is honoured, so a
// branch closed before it lands is aborted and stays openable rather than
// being left empty. Fetched rows join the data set, so they sort, filter and
// export like any other, and a branch is fetched once however often it toggles.
function fetchChildren(node, signal) {
return new Promise((resolve, reject) => {
if (signal.aborted) return reject(new DOMException('Aborted', 'AbortError'));
const leaf = node.kind === 'Site';
const timer = setTimeout(() => {
signal.removeEventListener('abort', onAbort);
resolve(Array.from({ length: node.childCount }, (_, i) => ({
id: `${node.id}-${i}`,
parentId: node.id,
name: leaf ? `ETH-${i}` : `Site ${100 + i}`,
kind: leaf ? 'Circuit' : 'Site',
childCount: leaf ? 0 : 3,
devices: leaf ? 2 : undefined,
charge: leaf ? 800 : undefined,
})));
}, 700);
function onAbort() { clearTimeout(timer); reject(new DOMException('Aborted', 'AbortError')); }
signal.addEventListener('abort', onAbort, { once: true });
});
}
const columns = [
{ field: 'kind', title: 'Level', filter: { type: 'set' }, layout: { width: 120 } },
{ field: 'childCount', title: 'Children', type: 'number', layout: { width: 120 } },
{ field: 'devices', title: 'Devices', type: 'number', total: 'sum', layout: { width: 120 } },
{ field: 'charge', title: 'Monthly charge', type: 'number', format: money, total: 'sum', layout: { width: 180 } },
];
// Eight roots, nothing below them until a branch is opened. Each root names a
// childCount but holds no children yet.
const rows = [/* region roots, each { id, parentId: null, name, kind: 'Region', childCount } */];
const tree = {
parentKey: 'parentId',
label: 'name',
title: 'Region',
// Without this the roots would have no expander at all: they hold no
// children, so there would be no gesture left to fetch any. A node that
// declares children reads as closed until they arrive.
hasChildren: (data) => data.childCount > 0,
loadChildren: (row, signal) => fetchChildren(row.data, signal),
};
function App() {
return (
<LatticeGrid
rowKey="id"
selection="multiple"
tree={tree}
toolPanel={{ side: 'left', panels: ['columns', 'filters', 'views', 'quick'],
actions: ['undo', 'redo', 'export', 'restore', 'maximise'], exportName: 'lattice-demo' }}
columns={columns}
rows={rows}
style={{ height: '540px' }}
/>
);
}
createRoot(document.getElementById('grid')).render(<App />);
</script>
Fetching tree branches on demand
Lazy children load a branch of a tree only when a row is expanded, rather than pulling the whole hierarchy up front. A developer reaches for this when the source is a remote API rather than an in-memory array: an org chart where each manager’s direct reports live behind a separate endpoint, a file browser where a folder’s contents are unknown until it is opened, a category tree too large to fetch in one request. Lattice Grid calls tree.loadChildren for the row being expanded, passing its id, and expects a promise resolving to that row’s children; the branch renders once the promise settles, and a distinct loading state occupies the row in between so the grid never presents an empty branch as an empty result. Closing a branch before its fetch resolves aborts the in-flight request through the same signal loadChildren receives, so a fast double-click through several folders does not leave stale fetches racing to populate rows the user has already collapsed. Because this is a JavaScript data grid built around incremental rendering, only the expanded branch pays the network cost; sibling rows and the rest of the tree keep their existing state untouched. The loading row exposes aria-busy while a fetch is pending, so a screen reader announces that a branch is populating rather than reporting silence.
How do I load tree children from an API only when a row expands?
Implement tree.loadChildren, returning a promise that resolves to the array of child rows for the expanded row’s id. Lattice Grid calls it on expand, shows a loading row until the promise resolves, and aborts the request if the branch is collapsed again before the fetch completes, so no children are rendered for a branch the user has already closed.