diff --git a/packages/react-components/react-tree/stories/D_flatTree/TreeInfiniteScrolling.md b/packages/react-components/react-tree/stories/D_flatTree/TreeInfiniteScrolling.md new file mode 100644 index 00000000000000..c2eeb7b91cc6c4 --- /dev/null +++ b/packages/react-components/react-tree/stories/D_flatTree/TreeInfiniteScrolling.md @@ -0,0 +1,5 @@ +The `InfiniteScrolling` example of the `Tree` component provides a dynamic and efficient Tree structure that loads more data as the user scrolls down the list. It uses the `useFlatTree` hook to manage a flat array of tree items, converting them into a hierarchical tree structure as needed. + +When the `Tree` is first rendered, a set number of `TreeItem` components are displayed, ensuring fast load times and efficient handling of potentially large amounts of data. As the user scrolls down the list, the `onScroll` event triggers the loading of more `TreeItem` components. + +This approach not only enhances the scalability of your application, but also improves the user experience by loading data as and when it is needed. The user is not overwhelmed with all the data at once and does not have to wait for large amounts of data to load initially. diff --git a/packages/react-components/react-tree/stories/D_flatTree/TreeInfiniteScrolling.stories.tsx b/packages/react-components/react-tree/stories/D_flatTree/TreeInfiniteScrolling.stories.tsx new file mode 100644 index 00000000000000..7b5e8877fa0b1f --- /dev/null +++ b/packages/react-components/react-tree/stories/D_flatTree/TreeInfiniteScrolling.stories.tsx @@ -0,0 +1,124 @@ +import * as React from 'react'; +import { Tree, TreeItem, TreeItemLayout, useFlatTree_unstable, FlatTreeItemProps } from '@fluentui/react-tree'; +import { makeStyles, shorthands, Spinner } from '@fluentui/react-components'; +import story from './TreeInfiniteScrolling.md'; + +interface Result { + results: { name: string }[]; +} + +type Item = FlatTreeItemProps & { name: string | React.ReactNode }; + +const MAX_PAGES = 5; + +const pinnedItems = [ + { value: 'pinned', name: 'Pinned', id: 'pinned' }, + { value: 'pinned-item-1', parentValue: 'pinned', name: 'Pinned item 1' }, + { value: 'pinned-item-2', parentValue: 'pinned', name: 'Pinned item 2' }, + { value: 'pinned-item-3', parentValue: 'pinned', name: 'Pinned item 3' }, +]; + +const useStyles = makeStyles({ + container: { + height: '400px', + paddingBottom: '10px', + ...shorthands.overflow('auto'), + }, +}); + +export const InfiniteScrolling = () => { + const [page, setPage] = React.useState(1); + const [isLoading, setIsLoading] = React.useState(false); + const peopleItems = useQuery([ + { value: 'people', name: 'People' }, + ...Array.from({ length: 40 }, (_, index) => ({ + value: `person-${index + 1}`, + parentValue: 'people', + name: `Person ${index + 1}`, + })), + ]); + + const items = React.useMemo( + () => [ + ...pinnedItems, + ...peopleItems.value, + ...(isLoading + ? [ + { + value: 'loading-people', + parentValue: 'people', + name: , + }, + ] + : []), + ], + [isLoading, peopleItems], + ); + + const styles = useStyles(); + + const flatTree = useFlatTree_unstable(items, { defaultOpenItems: ['pinned', 'people'] }); + + const fetchMoreItems = () => { + setIsLoading(true); + + fetch(`https://swapi.dev/api/people?page=${page}`) + .then(res => res.json()) + .then((json: Result) => { + const fetchedItems = json.results.map(person => ({ + value: `person-${person.name}`, + parentValue: 'people', + name: person.name, + })); + + setIsLoading(false); + setPage(page + 1); + peopleItems.query(() => [...peopleItems.value, ...fetchedItems]); + }); + }; + + const handleScroll = (event: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; + const hasReachedEnd = scrollHeight - scrollTop === clientHeight; + + if (!isLoading && hasReachedEnd && page < MAX_PAGES) { + fetchMoreItems(); + } + }; + + return ( + + {Array.from(flatTree.items(), flatTreeItem => { + const { name, ...treeItemProps } = flatTreeItem.getTreeItemProps(); + return ( + + {name} + + ); + })} + + ); +}; + +/** + * This function is just for the sake of the example, + * a library for fetching data (like react-query) might be a better option + */ +function useQuery(initialValue: Value) { + const [queryResult, setQueryResult] = React.useState({ value: initialValue, isLoading: false, isLoaded: false }); + const query = (fn: () => Promise | Value) => { + setQueryResult(curr => ({ ...curr, isLoading: true })); + Promise.resolve(fn()).then(nextValue => { + setQueryResult({ value: nextValue, isLoaded: true, isLoading: false }); + }); + }; + return { ...queryResult, query } as const; +} + +InfiniteScrolling.parameters = { + docs: { + description: { + story, + }, + }, +}; diff --git a/packages/react-components/react-tree/stories/D_flatTree/index.stories.tsx b/packages/react-components/react-tree/stories/D_flatTree/index.stories.tsx index eb95c8aa5cd060..c538b33ca0d81f 100644 --- a/packages/react-components/react-tree/stories/D_flatTree/index.stories.tsx +++ b/packages/react-components/react-tree/stories/D_flatTree/index.stories.tsx @@ -6,6 +6,7 @@ export { FlattenTree as flattenTree } from './flattenTree.stories'; export { Virtualization } from './Virtualization.stories'; export { AddRemoveTreeItem } from './TreeItemAddRemove.stories'; export { LazyLoading } from './TreeLazyLoading.stories'; +export { InfiniteScrolling } from './TreeInfiniteScrolling.stories'; export default { title: 'Preview Components/Tree/flatTree',