Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TreeView] Add lazy loading for children of tree items #15308

Merged
merged 64 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
0918085
add getChildrenCount
noraleonte Oct 28, 2024
45779ba
wip
noraleonte Oct 31, 2024
a29e2d0
wip
noraleonte Nov 6, 2024
765c2a5
wip
noraleonte Nov 6, 2024
7a3fad1
wip
noraleonte Nov 6, 2024
80bcc27
docs wip
noraleonte Nov 6, 2024
3e1e022
wip docs
noraleonte Nov 6, 2024
ea5a955
wip
noraleonte Nov 14, 2024
cf18e16
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Nov 15, 2024
ef26a40
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Nov 20, 2024
1aff38e
add selector
noraleonte Nov 20, 2024
5b9f5f8
wip
noraleonte Nov 20, 2024
addcdb8
wip
noraleonte Nov 20, 2024
c99a9dd
fix expansion and adjust initial state
noraleonte Nov 22, 2024
620578e
add loading and error for tree view
noraleonte Nov 22, 2024
26ebc5a
update error demo
noraleonte Nov 22, 2024
0d54f50
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Dec 3, 2024
aeefe05
rerun selection if needed
noraleonte Dec 3, 2024
081d8a3
check usability of dnd
noraleonte Dec 3, 2024
c31a456
rename treeViewDataSource -> dataSource
noraleonte Dec 3, 2024
ce5f0c5
fix initial state and error demos
noraleonte Dec 4, 2024
70a2fb8
add react-query demos
noraleonte Dec 5, 2024
04466c3
fix label editing
noraleonte Dec 9, 2024
6408dcb
wip
noraleonte Dec 9, 2024
0f5c1bd
fix label editing
noraleonte Dec 9, 2024
88dc0e8
move to pro
noraleonte Dec 9, 2024
321f004
update meta
noraleonte Dec 9, 2024
c552110
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Dec 9, 2024
fd58f94
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Dec 10, 2024
d449f19
docs:api
noraleonte Dec 10, 2024
6cc9de7
fix ts
noraleonte Dec 10, 2024
996a1b8
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Dec 10, 2024
a8a603c
add JSDocs
noraleonte Dec 10, 2024
90ff8b1
fix
noraleonte Dec 11, 2024
c5802a1
add tests
noraleonte Dec 11, 2024
9cbbd44
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Dec 11, 2024
a9bcc3d
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Dec 16, 2024
25ce29d
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Dec 23, 2024
70581b0
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Jan 7, 2025
8c96d04
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Jan 20, 2025
476dc72
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Jan 27, 2025
ec1c2e7
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Feb 6, 2025
651127a
update query library
noraleonte Feb 6, 2025
999a3b7
remove random line
noraleonte Feb 6, 2025
8f94e35
update editing md
noraleonte Feb 6, 2025
e91559b
fix
noraleonte Feb 6, 2025
d911bb0
fix
noraleonte Feb 6, 2025
38499c3
updates batch 1
noraleonte Feb 10, 2025
dad833b
batch 2
noraleonte Feb 10, 2025
7cb2532
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Feb 10, 2025
4f78bc9
Fix TS
flaviendelangle Feb 11, 2025
d1b573a
add slot for item error and loading state
noraleonte Feb 11, 2025
11f66ac
Merge branch 'lazy-loading-poc' of github.com:noraleonte/mui-x into l…
noraleonte Feb 11, 2025
e734f78
scripts
noraleonte Feb 11, 2025
f9fcea6
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Feb 11, 2025
016d256
fix
noraleonte Feb 11, 2025
b9df1b3
rename updateItemsState
noraleonte Feb 11, 2025
2973a25
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Feb 12, 2025
4796b78
fix typing issue
noraleonte Feb 12, 2025
1807791
fix
noraleonte Feb 12, 2025
2ea1dd3
fix ts errors
noraleonte Feb 19, 2025
d7f8c49
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Feb 19, 2025
4cf46d5
Merge branch 'master' of github.com:noraleonte/mui-x into lazy-loadin…
noraleonte Feb 26, 2025
bb5d8f5
update docs
noraleonte Feb 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/data/data-grid/list-view/components/ListCell.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export function ListCell(props) {
{showSize && <CardDetail>{formatSize(params.row.size)}</CardDetail>}
</CardDetailList>
)}

{density === 'comfortable' && (showCreatedAt || showUpdatedAt) && (
<CardDetail>
{showUpdatedAt && `Updated ${formatDate(params.row.updatedAt)}`}
Expand Down
1 change: 1 addition & 0 deletions docs/data/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ const pages: MuiPage[] = [
{ pathname: '/x/react-tree-view/rich-tree-view/customization' },
{ pathname: '/x/react-tree-view/rich-tree-view/focus' },
{ pathname: '/x/react-tree-view/rich-tree-view/editing' },
{ pathname: '/x/react-tree-view/rich-tree-view/lazy-loading' },
{ pathname: '/x/react-tree-view/rich-tree-view/ordering', plan: 'pro' },
],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { randomInt, randomName, randomId } from '@mui/x-data-grid-generator';
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';

const fetchData = async () => {
const rows = Array.from({ length: 10 }, () => ({
id: randomId(),
label: randomName({}, {}),
...(randomInt(0, 1) ? { childrenCount: 10 } : {}),
}));

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

export default function BasicLazyLoading() {
return (
<Box sx={{ width: '300px' }}>
<RichTreeView
items={[]}
experimentalFeatures={{ lazyLoading: true }}
treeViewDataSource={{
getChildrenCount: (item) => item?.childrenCount,
getTreeItems: fetchData,
}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not super clear to me if:

  1. This should be part of MUI X and not Toolpad. But maybe [data grid] Provide ready to use data sources #14731 is another layer of abstraction, on top of this one
  2. How much code should be shared with the data grid

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now we are following a DX that is close to the one of the grid.
Once we have an initial version fully working, we will see with the grid and the charts if there are things to share with the grid. Sharing DX is obvious (to me at least), sharing code might be more complex since it's very tight to the internals of each set of components.

/>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { randomInt, randomName, randomId } from '@mui/x-data-grid-generator';
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
import { TreeViewBaseItem } from '@mui/x-tree-view/models';

const fetchData = async (): Promise<
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const fetchData = async (): Promise<
async function fetchData(): Promise<

TreeViewBaseItem<{
id: string;
label: string;
childrenCount?: number;
}>[]
> => {
const rows = Array.from({ length: 10 }, () => ({
id: randomId(),
label: randomName({}, {}),
...(randomInt(0, 1) ? { childrenCount: 10 } : {}),
}));

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

export default function BasicLazyLoading() {
return (
<Box sx={{ width: '300px' }}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Box sx={{ width: '300px' }}>
<Box sx={{ minHeight: 320, minWidth: 300 }}>

<RichTreeView
items={[]}
experimentalFeatures={{ lazyLoading: true }}
treeViewDataSource={{
getChildrenCount: (item) => item?.childrenCount as number,
getTreeItems: fetchData,
}}
/>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<RichTreeView
items={[]}
experimentalFeatures={{ lazyLoading: true }}
treeViewDataSource={{
getChildrenCount: (item) => item?.childrenCount as number,
getTreeItems: fetchData,
}}
/>
38 changes: 38 additions & 0 deletions docs/data/tree-view/rich-tree-view/lazy-loading/ErrorManagement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { randomName, randomId } from '@mui/x-data-grid-generator';
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';

const fetchData = async (parentId) => {
const rows = Array.from({ length: 10 }, () => ({
id: randomId(),
label: randomName({}, {}),
childrenCount: 10,
}));

// make the promise fail if the item has a parent
return new Promise((resolve, reject) => {
setTimeout(() => {
if (parentId) {
reject(new Error('Error fetching data'));
} else {
resolve(rows);
}
}, 1000);
});
};

export default function ErrorManagement() {
return (
<Box sx={{ width: '300px' }}>
<RichTreeView
items={[]}
experimentalFeatures={{ lazyLoading: true }}
treeViewDataSource={{
getChildrenCount: (item) => item?.childrenCount,
getTreeItems: fetchData,
}}
/>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { randomName, randomId } from '@mui/x-data-grid-generator';
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
import { TreeViewBaseItem, TreeViewItemId } from '@mui/x-tree-view/models';

const fetchData = async (
parentId?: TreeViewItemId,
): Promise<
TreeViewBaseItem<{
id: string;
label: string;
childrenCount?: number;
}>[]
> => {
const rows = Array.from({ length: 10 }, () => ({
id: randomId(),
label: randomName({}, {}),
childrenCount: 10,
}));

// make the promise fail if the item has a parent
return new Promise((resolve, reject) => {
setTimeout(() => {
if (parentId) {
reject(new Error('Error fetching data'));
} else {
resolve(rows);
}
}, 1000);
});
};

export default function ErrorManagement() {
return (
<Box sx={{ width: '300px' }}>
<RichTreeView
items={[]}
experimentalFeatures={{ lazyLoading: true }}
treeViewDataSource={{
getChildrenCount: (item) => item?.childrenCount as number,
getTreeItems: fetchData,
}}
/>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<RichTreeView
items={[]}
experimentalFeatures={{ lazyLoading: true }}
treeViewDataSource={{
getChildrenCount: (item) => item?.childrenCount as number,
getTreeItems: fetchData,
}}
/>
38 changes: 38 additions & 0 deletions docs/data/tree-view/rich-tree-view/lazy-loading/LowTTLCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { randomInt, randomName, randomId } from '@mui/x-data-grid-generator';
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';

import { TreeViewDataSourceCacheDefault } from '@mui/x-tree-view/utils';

const fetchData = async () => {
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 TreeViewDataSourceCacheDefault({ ttl: 1000 * 10 }); // 10 seconds

export default function LowTTLCache() {
return (
<Box sx={{ width: '300px' }}>
<RichTreeView
items={[]}
experimentalFeatures={{ lazyLoading: true }}
treeViewDataSource={{
getChildrenCount: (item) => item?.childrenCount,
getTreeItems: fetchData,
}}
treeViewDataSourceCache={lowTTLCache}
/>
</Box>
);
}
44 changes: 44 additions & 0 deletions docs/data/tree-view/rich-tree-view/lazy-loading/LowTTLCache.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { randomInt, randomName, randomId } from '@mui/x-data-grid-generator';
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
import { TreeViewBaseItem } from '@mui/x-tree-view/models';
import { TreeViewDataSourceCacheDefault } from '@mui/x-tree-view/utils';

const fetchData = async (): Promise<
TreeViewBaseItem<{
id: string;
label: string;
childrenCount?: number;
}>[]
> => {
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 TreeViewDataSourceCacheDefault({ ttl: 1000 * 10 }); // 10 seconds

export default function LowTTLCache() {
return (
<Box sx={{ width: '300px' }}>
<RichTreeView
items={[]}
experimentalFeatures={{ lazyLoading: true }}
treeViewDataSource={{
getChildrenCount: (item) => item?.childrenCount as number,
getTreeItems: fetchData,
}}
treeViewDataSourceCache={lowTTLCache}
/>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<RichTreeView
items={[]}
experimentalFeatures={{ lazyLoading: true }}
treeViewDataSource={{
getChildrenCount: (item) => item?.childrenCount as number,
getTreeItems: fetchData,
}}
treeViewDataSourceCache={lowTTLCache}
/>
36 changes: 36 additions & 0 deletions docs/data/tree-view/rich-tree-view/lazy-loading/lazy-loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
productId: x-tree-view
title: Rich Tree View - Lazy Loading Children
components: RichTreeView, TreeItem
packageName: '@mui/x-tree-view'
githubLabel: 'component: tree view'
waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
---

# Rich Tree View - Lazy Loading Children
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Rich Tree View - Lazy Loading Children
# Rich Tree View - Lazy loading children [<span class="plan-pro"></span>](/x/introduction/licensing/#pro-plan 'Pro plan')

Copy link
Member

@flaviendelangle flaviendelangle Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep I agree, it should be part of the pro plan
I saw the grid discussing moving some lazy loading to the community package, we should just check with them before moving ours to the pro that we aren't doing a mistake, but AFAIK the feature should be rpo

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@oliviertassinari oliviertassinari Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why have data grid data source MIT but tree view data source Pro?

In the case of the data grid data: data source allows to maximize the open source adoption (get to a fully working prototype sooner, the fastest we get there, the least we give people time to try something else, I don't think people say I have 10h, I will try as many grids as possible, but instead, give a 2hrs budget to the first grid, if they feel too slow, kill it, try the next one, and so one) and even if you have the feature, you can't do much with it, you still need the Pro plan for their features. So MIT.

In the case of the Tree View: I would expect developers to be able to implement lazy loading using React Query (TanStack/query#413) with our MIT plan and not to have a too terrible experience. I assumed that our new docs page is about doing more than this, but looking closer, it's not so clear. So maybe the right move is:

  1. Make the page MIT, since we likely want to have an example using TanStack Query https://npm-stat.com/charts.html?package=react-dom&package=%40tanstack%2Freact-query&from=2023-11-12&to=2024-11-12?
  2. Add that example
  3. Rely on the fact that once people integrate lazy loading, they are more likely to hit a point where they need virtualization (Pro) or need drag & drop (Pro), and we need Lazy loading to be competitive in the MIT space, e.g. https://ant.design/components/tree#tree-demo-dynamic. This is a better 📈 ramp.
  4. We see if there is space for a more complex data fetching logic. Maybe we don't need a data source, if we do, it goes Pro.

Guesses our where a data source could truly shine for cases where:

  • the simple lazy loading API isn't enough
  • you are using a data grid data source, you are used to it, and you want the same abstraction
  • you are going to use virtualization, and you need to be smart about loading
  • SWR or React Query have too many limitations (we need to be careful to not rebuild those libraries but to be truly complementary). A quick research on what SWR does that fetch doesn't: https://www.reddit.com/r/nextjs/comments/17603jy/comment/k4j3stp/.


<p class="description">Lazy load the data from your Tree View.</p>

## Basic usage

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

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 for some reason, but there are some children, returns -1.

{{"demo": "BasicLazyLoading.js"}}

## Data caching

### Custom cache

### Customize the cache lifetime

The `TreeViewDataSourceCacheDefault` has a default Time To Live (`ttl`) of 5 minutes. To customize it, pass the ttl option in milliseconds to the `TreeViewDataSourceCacheDefault` constructor, and then pass it as the `treeViewDataSourceCache` prop.

{{"demo": "LowTTLCache.js"}}

## Error management

{{"demo": "ErrorManagement.js"}}
2 changes: 1 addition & 1 deletion docs/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/basic-features/typescript for more information.
7 changes: 7 additions & 0 deletions docs/pages/x/react-tree-view/rich-tree-view/lazy-loading.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';
import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs';
import * as pageProps from 'docsx/data/tree-view/rich-tree-view/lazy-loading/lazy-loading.md?muiMarkdown';

export default function Page() {
return <MarkdownDocs {...pageProps} />;
}
20 changes: 11 additions & 9 deletions packages/x-data-grid-pro/src/hooks/features/dataSource/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,18 @@ export class NestedDataManager {
this.queuedRequests.add(id);
loadingIds[id] = true;
});
this.api.setState((state) => ({
...state,
dataSource: {
...state.dataSource,
loading: {
...state.dataSource.loading,
...loadingIds,
this.api.setState((state) => {
return {
...state,
dataSource: {
...state.dataSource,
loading: {
...state.dataSource.loading,
...loadingIds,
},
},
},
}));
};
});
this.processQueue();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import {
useTreeViewLabel,
UseTreeViewLabelParameters,
} from '../internals/plugins/useTreeViewLabel';
import {
useTreeViewLazyLoading,
UseTreeViewLazyLoadingParameters,
} from '../internals/plugins/useTreeViewLazyLoading';

export const RICH_TREE_VIEW_PLUGINS = [
useTreeViewItems,
Expand All @@ -34,6 +38,7 @@ export const RICH_TREE_VIEW_PLUGINS = [
useTreeViewKeyboardNavigation,
useTreeViewIcons,
useTreeViewLabel,
useTreeViewLazyLoading,
] as const;

export type RichTreeViewPluginSignatures = ConvertPluginsIntoSignatures<
Expand All @@ -58,4 +63,5 @@ export interface RichTreeViewPluginParameters<R extends {}, Multiple extends boo
UseTreeViewFocusParameters,
UseTreeViewSelectionParameters<Multiple>,
UseTreeViewIconsParameters,
UseTreeViewLabelParameters<R> {}
UseTreeViewLabelParameters<R>,
UseTreeViewLazyLoadingParameters<R> {}
Loading
Loading