diff --git a/packages/react-core/src/components/TreeView/TreeView.tsx b/packages/react-core/src/components/TreeView/TreeView.tsx index 469e4a6621e..74137f6c538 100644 --- a/packages/react-core/src/components/TreeView/TreeView.tsx +++ b/packages/react-core/src/components/TreeView/TreeView.tsx @@ -69,6 +69,8 @@ export interface TreeViewProps { className?: string; /** Toolbar to display above the tree view */ toolbar?: React.ReactNode; + /** Flag indicating the TreeView should utilize memoization to help render large data sets. Setting this property requires that `activeItems` pass in an array containing every node in the selected item's path. */ + useMemo?: boolean; } export const TreeView: React.FunctionComponent = ({ @@ -89,6 +91,7 @@ export const TreeView: React.FunctionComponent = ({ activeItems, compareItems = (item, itemToCheck) => item.id === itemToCheck.id, className, + useMemo, ...props }: TreeViewProps) => { const treeViewList = ( @@ -116,6 +119,7 @@ export const TreeView: React.FunctionComponent = ({ action={item.action} compareItems={compareItems} isCompact={variant === 'compact' || variant === 'compactNoBackground'} + useMemo={useMemo} {...(item.children && { children: ( boolean; + /** Flag indicating the TreeView should utilize memoization to help render large data sets. Setting this property requires that `activeItems` pass in an array containing every node in the selected item's path. */ + useMemo?: boolean; } -export const TreeViewListItem: React.FunctionComponent = ({ +const TreeViewListItemBase: React.FunctionComponent = ({ name, title, id, @@ -78,10 +80,11 @@ export const TreeViewListItem: React.FunctionComponent = icon, expandedIcon, action, - compareItems + compareItems, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + useMemo }: TreeViewListItemProps) => { const [internalIsExpanded, setIsExpanded] = useState(defaultExpanded); - useEffect(() => { if (isExpanded !== undefined && isExpanded !== null) { setIsExpanded(isExpanded); @@ -207,4 +210,53 @@ export const TreeViewListItem: React.FunctionComponent = ); }; + +export const TreeViewListItem = React.memo(TreeViewListItemBase, (prevProps, nextProps) => { + if (!nextProps.useMemo) { + return false; + } + + const prevIncludes = + prevProps.activeItems && + prevProps.activeItems.length > 0 && + prevProps.activeItems.some( + item => prevProps.compareItems && item && prevProps.compareItems(item, prevProps.itemData) + ); + const nextIncludes = + nextProps.activeItems && + nextProps.activeItems.length > 0 && + nextProps.activeItems.some( + item => nextProps.compareItems && item && nextProps.compareItems(item, nextProps.itemData) + ); + + if (prevIncludes || nextIncludes) { + return false; + } + + if ( + prevProps.name !== nextProps.name || + prevProps.title !== nextProps.title || + prevProps.id !== nextProps.id || + prevProps.isExpanded !== nextProps.isExpanded || + prevProps.defaultExpanded !== nextProps.defaultExpanded || + prevProps.onSelect !== nextProps.onSelect || + prevProps.onCheck !== nextProps.onCheck || + prevProps.hasCheck !== nextProps.hasCheck || + prevProps.checkProps !== nextProps.checkProps || + prevProps.hasBadge !== nextProps.hasBadge || + prevProps.customBadgeContent !== nextProps.customBadgeContent || + prevProps.badgeProps !== nextProps.badgeProps || + prevProps.isCompact !== nextProps.isCompact || + prevProps.icon !== nextProps.icon || + prevProps.expandedIcon !== nextProps.expandedIcon || + prevProps.action !== nextProps.action || + prevProps.parentItem !== nextProps.parentItem || + prevProps.itemData !== nextProps.itemData + ) { + return false; + } + + return true; +}); + TreeViewListItem.displayName = 'TreeViewListItem'; diff --git a/packages/react-core/src/components/TreeView/examples/TreeView.md b/packages/react-core/src/components/TreeView/examples/TreeView.md index 11bddae1417..518012b79ff 100644 --- a/packages/react-core/src/components/TreeView/examples/TreeView.md +++ b/packages/react-core/src/components/TreeView/examples/TreeView.md @@ -212,16 +212,21 @@ class SearchTreeView extends React.Component { render() { const { activeItems, filteredItems, isFiltered } = this.state; - + const toolbar = ( - - - - + + + + - - ); + + ); return ( this.mapTree(item)); - return ( - - ); + return ; } } ``` @@ -700,7 +703,14 @@ class CustomBadgesTreeView extends React.Component { name: 'Sources', id: 'Sources', customBadgeContent: '1 source', - children: [{ name: 'Application 4', id: 'App4', customBadgeContent: '1 child', children: [{ name: 'Settings', id: 'App4Settings' }] }] + children: [ + { + name: 'Application 4', + id: 'App4', + customBadgeContent: '1 child', + children: [{ name: 'Settings', id: 'App4Settings' }] + } + ] }, { name: 'Really really really long folder name that overflows the container it is in', @@ -853,64 +863,64 @@ import { TreeView, TreeViewDataItem } from '@patternfly/react-core'; const GuidesTreeView: React.FunctionComponent = () => { const options: TreeViewDataItem[] = [ - { - name: 'Application launcher', - id: 'AppLaunch', - children: [ - { - name: 'Application 1', - id: 'App1', - children: [ - { name: 'Settings', id: 'App1Settings' }, - { name: 'Current', id: 'App1Current' } - ] - }, - { - name: 'Application 2', - id: 'App2', - children: [ - { name: 'Settings', id: 'App2Settings' }, - { - name: 'Loader', - id: 'App2Loader', - children: [ - { name: 'Loading App 1', id: 'LoadApp1' }, - { name: 'Loading App 2', id: 'LoadApp2' }, - { name: 'Loading App 3', id: 'LoadApp3' } - ] - } - ] - } - ], - defaultExpanded: true - }, - { - name: 'Cost management', - id: 'Cost', - children: [ - { - name: 'Application 3', - id: 'App3', - children: [ - { name: 'Settings', id: 'App3Settings' }, - { name: 'Current', id: 'App3Current' } - ] - } - ] - }, - { - name: 'Sources', - id: 'Sources', - children: [{ name: 'Application 4', id: 'App4', children: [{ name: 'Settings', id: 'App4Settings' }] }] - }, - { - name: 'Really really really long folder name that overflows the container it is in', - id: 'Long', - children: [{ name: 'Application 5', id: 'App5' }] - } - ]; - return ; -} + { + name: 'Application launcher', + id: 'AppLaunch', + children: [ + { + name: 'Application 1', + id: 'App1', + children: [ + { name: 'Settings', id: 'App1Settings' }, + { name: 'Current', id: 'App1Current' } + ] + }, + { + name: 'Application 2', + id: 'App2', + children: [ + { name: 'Settings', id: 'App2Settings' }, + { + name: 'Loader', + id: 'App2Loader', + children: [ + { name: 'Loading App 1', id: 'LoadApp1' }, + { name: 'Loading App 2', id: 'LoadApp2' }, + { name: 'Loading App 3', id: 'LoadApp3' } + ] + } + ] + } + ], + defaultExpanded: true + }, + { + name: 'Cost management', + id: 'Cost', + children: [ + { + name: 'Application 3', + id: 'App3', + children: [ + { name: 'Settings', id: 'App3Settings' }, + { name: 'Current', id: 'App3Current' } + ] + } + ] + }, + { + name: 'Sources', + id: 'Sources', + children: [{ name: 'Application 4', id: 'App4', children: [{ name: 'Settings', id: 'App4Settings' }] }] + }, + { + name: 'Really really really long folder name that overflows the container it is in', + id: 'Long', + children: [{ name: 'Application 5', id: 'App5' }] + } + ]; + return ; +}; ``` ### Compact @@ -921,61 +931,67 @@ import { TreeView, TreeViewDataItem } from '@patternfly/react-core'; const CompactTreeView: React.FunctionComponent = () => { const options: TreeViewDataItem[] = [ - { - name: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value and may reject unrecognized values.', - title: 'apiVersion', - id: 'apiVersion' - }, - { - name: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated is CamelCase. More info:', - title: 'kind', - id: 'kind' - }, - { - name: 'Standard metadata object', - title: 'metadata', - id: 'metadata' - }, - { - name: 'Standard metadata object', - title: 'spec', - id: 'spec', - children: [ - { - name: 'Minimum number of seconds for which a newly created pod should be ready without any of its container crashing, for it to be considered available. Default to 0 (pod will be considered available as soon as it is ready).', - title: 'minReadySeconds', - id: 'minReadySeconds' - }, - { - name: 'Indicates that the deployment is paused', - title: 'paused', - id: 'paused' - }, - { - name: 'The maximum time in seconds for a deployment to make progress before it is considered to be failed. The deployment controller will continue to process failed deployments and a condition with a ProgressDeadlineExceeded reason will be surfaced in the deployment status. Note that the progress will not de estimated during the time a deployment is paused. Defaults to 600s.', - title: 'progressDeadlineSeconds', - id: 'progressDeadlineSeconds', - children: [ - { - name: 'The number of old ReplicaSets to retain to allow rollback. This is a pointer to distinguish between explicit zero and not specified. Defaults to 10.', - title: 'revisionHistoryLimit', - id: 'revisionHistoryLimit', - children: [ - { - name: 'Map of {key.value} pairs. A single {key.value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In" and the values array contains only "value". The requirements are ANDed.', - title: 'matchLabels', - id: 'matchLabels' - } - ] - } - ] - } - ] - } - ]; - return ; -} -``` + { + name: + 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value and may reject unrecognized values.', + title: 'apiVersion', + id: 'apiVersion' + }, + { + name: + 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated is CamelCase. More info:', + title: 'kind', + id: 'kind' + }, + { + name: 'Standard metadata object', + title: 'metadata', + id: 'metadata' + }, + { + name: 'Standard metadata object', + title: 'spec', + id: 'spec', + children: [ + { + name: + 'Minimum number of seconds for which a newly created pod should be ready without any of its container crashing, for it to be considered available. Default to 0 (pod will be considered available as soon as it is ready).', + title: 'minReadySeconds', + id: 'minReadySeconds' + }, + { + name: 'Indicates that the deployment is paused', + title: 'paused', + id: 'paused' + }, + { + name: + 'The maximum time in seconds for a deployment to make progress before it is considered to be failed. The deployment controller will continue to process failed deployments and a condition with a ProgressDeadlineExceeded reason will be surfaced in the deployment status. Note that the progress will not de estimated during the time a deployment is paused. Defaults to 600s.', + title: 'progressDeadlineSeconds', + id: 'progressDeadlineSeconds', + children: [ + { + name: + 'The number of old ReplicaSets to retain to allow rollback. This is a pointer to distinguish between explicit zero and not specified. Defaults to 10.', + title: 'revisionHistoryLimit', + id: 'revisionHistoryLimit', + children: [ + { + name: + 'Map of {key.value} pairs. A single {key.value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In" and the values array contains only "value". The requirements are ANDed.', + title: 'matchLabels', + id: 'matchLabels' + } + ] + } + ] + } + ] + } + ]; + return ; +}; +``` ### Compact, no background @@ -985,58 +1001,152 @@ import { TreeView, TreeViewDataItem } from '@patternfly/react-core'; const CompactNoBackgroundTreeView: React.FunctionComponent = () => { const options: TreeViewDataItem[] = [ - { - name: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value and may reject unrecognized values.', - title: 'apiVersion', - id: 'apiVersion' - }, - { - name: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated is CamelCase. More info:', - title: 'kind', - id: 'kind' - }, - { - name: 'Standard metadata object', - title: 'metadata', - id: 'metadata' - }, - { - name: 'Standard metadata object', - title: 'spec', - id: 'spec', - children: [ - { - name: 'Minimum number of seconds for which a newly created pod should be ready without any of its container crashing, for it to be considered available. Default to 0 (pod will be considered available as soon as it is ready).', - title: 'minReadySeconds', - id: 'minReadySeconds' - }, - { - name: 'Indicates that the deployment is paused', - title: 'paused', - id: 'paused' - }, - { - name: 'The maximum time in seconds for a deployment to make progress before it is considered to be failed. The deployment controller will continue to process failed deployments and a condition with a ProgressDeadlineExceeded reason will be surfaced in the deployment status. Note that the progress will not de estimated during the time a deployment is paused. Defaults to 600s.', - title: 'progressDeadlineSeconds', - id: 'progressDeadlineSeconds', - children: [ - { - name: 'The number of old ReplicaSets to retain to allow rollback. This is a pointer to distinguish between explicit zero and not specified. Defaults to 10.', - title: 'revisionHistoryLimit', - id: 'revisionHistoryLimit', - children: [ - { - name: 'Map of {key.value} pairs. A single {key.value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In" and the values array contains only "value". The requirements are ANDed.', - title: 'matchLabels', - id: 'matchLabels' - } - ] - } - ] + { + name: + 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value and may reject unrecognized values.', + title: 'apiVersion', + id: 'apiVersion' + }, + { + name: + 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated is CamelCase. More info:', + title: 'kind', + id: 'kind' + }, + { + name: 'Standard metadata object', + title: 'metadata', + id: 'metadata' + }, + { + name: 'Standard metadata object', + title: 'spec', + id: 'spec', + children: [ + { + name: + 'Minimum number of seconds for which a newly created pod should be ready without any of its container crashing, for it to be considered available. Default to 0 (pod will be considered available as soon as it is ready).', + title: 'minReadySeconds', + id: 'minReadySeconds' + }, + { + name: 'Indicates that the deployment is paused', + title: 'paused', + id: 'paused' + }, + { + name: + 'The maximum time in seconds for a deployment to make progress before it is considered to be failed. The deployment controller will continue to process failed deployments and a condition with a ProgressDeadlineExceeded reason will be surfaced in the deployment status. Note that the progress will not de estimated during the time a deployment is paused. Defaults to 600s.', + title: 'progressDeadlineSeconds', + id: 'progressDeadlineSeconds', + children: [ + { + name: + 'The number of old ReplicaSets to retain to allow rollback. This is a pointer to distinguish between explicit zero and not specified. Defaults to 10.', + title: 'revisionHistoryLimit', + id: 'revisionHistoryLimit', + children: [ + { + name: + 'Map of {key.value} pairs. A single {key.value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In" and the values array contains only "value". The requirements are ANDed.', + title: 'matchLabels', + id: 'matchLabels' + } + ] + } + ] + } + ] + } + ]; + return ; +}; +``` + +### With memoization + +Turning on memoization with the `useMemo` property helps prevent unnecessary re-renders for large data sets. With this flag active, `activeItems` must pass in an array of nodes along the selected item's path to update properly. + +```js +import React from 'react'; +import { TreeView, Button } from '@patternfly/react-core'; + +class MemoTreeView extends React.Component { + constructor(props) { + super(props); + + this.state = { activeItems: {}, allExpanded: false }; + + this.onSelect = (evt, treeViewItem) => { + let filtered = []; + this.options.forEach(item => this.filterItems(item, treeViewItem.id, filtered)); + this.setState({ + activeItems: filtered + }); + }; + + this.onToggle = evt => { + const { allExpanded } = this.state; + this.setState({ + allExpanded: allExpanded !== undefined ? !allExpanded : true + }); + }; + + this.filterItems = (item, input, list) => { + if (item.children) { + let childContained = false; + item.children.forEach(child => { + if (childContained) { + this.filterItems(child, input, list); + } else { + childContained = this.filterItems(child, input, list); } - ] + }); + if (childContained) { + list.push(item); + } } - ]; - return ; + + if (item.id === input) { + list.push(item); + return true; + } else { + return false; + } + }; + + this.options = []; + for (let i = 1; i <= 20; i++) { + const childNum = 5; + let childOptions = []; + for (let j = 1; j <= childNum; j++) { + childOptions.push({ name: 'Child ' + j, id: `Option${i} - Child${j}` }); + } + this.options.push({ name: 'Option ' + i, id: i, children: childOptions }); + } + } + + render() { + const { activeItems, allExpanded } = this.state; + const tree = ( + + ); + + return ( + + + {tree} + + ); + } } -``` \ No newline at end of file +```