From 73dba52ebda3f74db9bc2565db7ce7362bb9b0ff Mon Sep 17 00:00:00 2001 From: Igor Dykhta Date: Fri, 30 Sep 2022 19:59:35 +0300 Subject: [PATCH] [Chore] Extra memoization for components to prevent re-rendering (#1988) --- src/components/src/common/field-selector.tsx | 14 ++-- src/components/src/common/histogram-plot.tsx | 29 +++++--- .../common/item-selector/item-selector.tsx | 7 +- src/components/src/editor/editor.tsx | 4 +- src/components/src/map-container.tsx | 71 +++++++++++-------- src/components/src/notification-panel.tsx | 13 +++- .../src/side-panel/filter-manager.tsx | 17 +++-- .../src/side-panel/layer-manager.tsx | 65 +++++++++-------- .../layer-panel/dataset-section.tsx | 6 +- .../src/side-panel/panel-header-action.tsx | 5 +- .../src/side-panel/panel-header.tsx | 55 +++++++------- 11 files changed, 169 insertions(+), 117 deletions(-) diff --git a/src/components/src/common/field-selector.tsx b/src/components/src/common/field-selector.tsx index 2e8d4d99ae..9b34054255 100644 --- a/src/components/src/common/field-selector.tsx +++ b/src/components/src/common/field-selector.tsx @@ -129,12 +129,16 @@ function FieldSelectorFactory( }; fieldsSelector = props => props.fields; - filteredFieldsSelector = props => - props.fields.filter( - field => - !toArray(props.value).find(d => (d.name ? d.name === field.name : d === field.name)) - ); valueSelector = props => props.value; + filteredFieldsSelector = createSelector( + this.fieldsSelector, + this.valueSelector, + (fields, value) => { + return fields.filter( + field => !toArray(value).find(d => (d.name ? d.name === field.name : d === field.name)) + ); + } + ); filterFieldTypesSelector = props => props.filterFieldTypes; showTokenSelector = props => props.showToken; diff --git a/src/components/src/common/histogram-plot.tsx b/src/components/src/common/histogram-plot.tsx index d731166e9a..d1f58cacc8 100644 --- a/src/components/src/common/histogram-plot.tsx +++ b/src/components/src/common/histogram-plot.tsx @@ -21,8 +21,8 @@ import React, {ReactElement, useMemo} from 'react'; import {scaleLinear} from 'd3-scale'; import {max} from 'd3-array'; +import {hcl} from 'd3-color'; import styled from 'styled-components'; -import classnames from 'classnames'; import {HistogramBin} from '@kepler.gl/types'; const histogramStyle = { @@ -32,16 +32,23 @@ const histogramStyle = { const HistogramWrapper = styled.svg` overflow: visible; - .histogram-bars { - rect { - fill: ${props => props.theme.histogramFillOutRange}; - } - rect.in-range { - fill: ${props => props.theme.histogramFillInRange}; - } - } `; +type BarType = { + inRange: boolean; +}; +const BarUnmemoized = styled.rect( + ({theme, inRange, color}) => ` + ${ + inRange + ? `fill: ${color ?? theme.histogramFillInRange};` + : `fill: ${color ? hcl(color).darker() : theme.histogramFillOutRange};` + } +` +); +const Bar = React.memo(BarUnmemoized); +Bar.displayName = 'Bar'; + interface HistogramPlotParams { width: number; height: number; @@ -99,8 +106,8 @@ function HistogramPlotFactory() { undefinedToZero(bar.x1) <= value[1] && undefinedToZero(bar.x0) >= value[0]; const wRatio = inRange ? histogramStyle.highlightW : histogramStyle.unHighlightedW; return ( - { +class ItemSelectorUnmemoized extends Component { static defaultProps = { multiSelect: true, placeholder: 'placeholder.enterValue', @@ -374,4 +374,7 @@ class ItemSelector extends Component { } } -export default injectIntl(ItemSelector); +const ItemSelector = React.memo(ItemSelectorUnmemoized); +ItemSelector.displayName = 'ItemSelector'; + +export default injectIntl(ItemSelectorUnmemoized); diff --git a/src/components/src/editor/editor.tsx b/src/components/src/editor/editor.tsx index 975c1a5f7d..ab1fc290a3 100644 --- a/src/components/src/editor/editor.tsx +++ b/src/components/src/editor/editor.tsx @@ -67,7 +67,7 @@ interface EditorProps { export default function EditorFactory( FeatureActionPanel: React.FC ): React.ComponentClass { - class Editor extends Component { + class EditorUnmemoized extends Component { static defaultProps = { clickRadius: DEFAULT_RADIUS }; @@ -227,5 +227,7 @@ export default function EditorFactory( } } + const Editor = (React.memo(EditorUnmemoized) as unknown) as typeof EditorUnmemoized; + Editor.displayName = 'Editor'; return Editor; } diff --git a/src/components/src/map-container.tsx b/src/components/src/map-container.tsx index c6898546d0..e851f66200 100644 --- a/src/components/src/map-container.tsx +++ b/src/components/src/map-container.tsx @@ -19,7 +19,7 @@ // THE SOFTWARE. // libraries -import React, {Component, createRef} from 'react'; +import React, {Component, createRef, useMemo} from 'react'; import MapboxGLMap, {MapRef} from 'react-map-gl'; import DeckGL from '@deck.gl/react'; import {createSelector, Selector} from 'reselect'; @@ -89,6 +89,8 @@ const MAP_STYLE: {[key: string]: React.CSSProperties} = { } }; +const LOCALE_CODES_ARRAY = Object.keys(LOCALE_CODES); + const MAPBOXGL_STYLE_UPDATE = 'style.load'; const MAPBOXGL_RENDER = 'render'; const nop = () => {}; @@ -108,37 +110,50 @@ const MapboxLogo = () => ( export const Attribution = ({showMapboxLogo = true}) => { const isPalm = hasMobileWidth(breakPointValues); - if (!showMapboxLogo) { + + const memoizedComponents = useMemo(() => { + if (!showMapboxLogo) { + return ( + + + © OpenStreetMap + + + ); + } + return ( - - © OpenStreetMap - + {isPalm ? : null} + ); - } + }, [showMapboxLogo, isPalm]); - return ( - - {isPalm ? : null} - - - ); + return memoizedComponents; }; MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory]; @@ -614,7 +629,7 @@ export default function MapContainerFactory( <> -) { - return class NotificationPanel extends Component { +): React.ComponentClass { + class NotificationPanelUnmemoized extends Component { + static displayName = 'NotificationPanel'; + render() { const globalNotifications = this.props.notifications.filter( n => n.topic === DEFAULT_NOTIFICATION_TOPICS.global @@ -71,5 +73,10 @@ export default function NotificationPanelFactory( ); } - }; + } + + const NotificationPanel = (React.memo( + NotificationPanelUnmemoized + ) as unknown) as typeof NotificationPanelUnmemoized; + return NotificationPanel; } diff --git a/src/components/src/side-panel/filter-manager.tsx b/src/components/src/side-panel/filter-manager.tsx index 1c1573a893..f23a5b988a 100644 --- a/src/components/src/side-panel/filter-manager.tsx +++ b/src/components/src/side-panel/filter-manager.tsx @@ -75,6 +75,15 @@ function FilterManagerFactory( .reverse(); }, [filters.length]); + const filterPanelCallbacks = useMemo(() => { + return new Array(filters.length).fill(0).map((d, idx) => ({ + removeFilter: () => removeFilter(idx), + enlargeFilter: () => enlargeFilter(idx), + toggleAnimation: () => toggleFilterAnimation(idx), + toggleFilterFeature: () => toggleFilterFeature(idx) + })); + }, [filters.length, removeFilter, enlargeFilter, toggleFilterAnimation, toggleFilterFeature]); + return (
removeFilter(idx)} - enlargeFilter={() => enlargeFilter(idx)} - toggleAnimation={() => toggleFilterAnimation(idx)} - toggleFilterFeature={() => toggleFilterFeature(idx)} + removeFilter={filterPanelCallbacks[idx].removeFilter} + enlargeFilter={filterPanelCallbacks[idx].enlargeFilter} + toggleAnimation={filterPanelCallbacks[idx].toggleAnimation} + toggleFilterFeature={filterPanelCallbacks[idx].toggleFilterFeature} setFilter={setFilter} /> ))} diff --git a/src/components/src/side-panel/layer-manager.tsx b/src/components/src/side-panel/layer-manager.tsx index 37cdab7ef5..9bf7cf13de 100644 --- a/src/components/src/side-panel/layer-manager.tsx +++ b/src/components/src/side-panel/layer-manager.tsx @@ -71,39 +71,38 @@ const LayerHeader = styled.div.attrs({ margin-top: 16px; `; -const LayerBlendingSelector = ({ - layerBlending, - updateLayerBlending, - intl -}: LayerBlendingSelectorProps) => { - const labeledLayerBlendings = Object.keys(LAYER_BLENDINGS).reduce( - (acc, current) => ({ - ...acc, - [intl.formatMessage({id: LAYER_BLENDINGS[current].label})]: current - }), - {} - ); - - const onChange = useCallback(blending => updateLayerBlending(labeledLayerBlendings[blending]), [ - updateLayerBlending, - labeledLayerBlendings - ]); - - return ( - - - - - - - ); -}; +const LayerBlendingSelector = React.memo( + ({layerBlending, updateLayerBlending, intl}: LayerBlendingSelectorProps) => { + const labeledLayerBlendings = Object.keys(LAYER_BLENDINGS).reduce( + (acc, current) => ({ + ...acc, + [intl.formatMessage({id: LAYER_BLENDINGS[current].label})]: current + }), + {} + ); + + const onChange = useCallback(blending => updateLayerBlending(labeledLayerBlendings[blending]), [ + updateLayerBlending, + labeledLayerBlendings + ]); + + return ( + + + + + + + ); + } +); +LayerBlendingSelector.displayName = 'LayerBlendingSelector'; LayerManagerFactory.deps = [ LayerListFactory, diff --git a/src/components/src/side-panel/layer-panel/dataset-section.tsx b/src/components/src/side-panel/layer-panel/dataset-section.tsx index 1f0a1784e3..a287aac55a 100644 --- a/src/components/src/side-panel/layer-panel/dataset-section.tsx +++ b/src/components/src/side-panel/layer-panel/dataset-section.tsx @@ -63,7 +63,7 @@ const StyledDatasetSection = styled.div` `; export function AddDataButtonFactory() { - const AddDataButton: React.FC = ({onClick, isInactive}) => ( + const AddDataButton: React.FC = React.memo(({onClick, isInactive}) => ( - ); - + )); + AddDataButton.displayName = 'AddDataButton'; return AddDataButton; } diff --git a/src/components/src/side-panel/panel-header-action.tsx b/src/components/src/side-panel/panel-header-action.tsx index a01655a173..1c522162ff 100644 --- a/src/components/src/side-panel/panel-header-action.tsx +++ b/src/components/src/side-panel/panel-header-action.tsx @@ -67,7 +67,7 @@ const HeaderActionWrapper = styled.div` PanelHeaderActionFactory.deps = []; // Need to use react class to access props.component export default function PanelHeaderActionFactory(): React.FC { - const PanelHeaderAction: React.FC = ({ + const PanelHeaderActionUnmemoized: React.FC = ({ onClick, tooltip, id, @@ -102,5 +102,8 @@ export default function PanelHeaderActionFactory(): React.FC ); }; + + const PanelHeaderAction = React.memo(PanelHeaderActionUnmemoized); + PanelHeaderAction.displayName = 'PanelHeaderAction'; return PanelHeaderAction; } diff --git a/src/components/src/side-panel/panel-header.tsx b/src/components/src/side-panel/panel-header.tsx index b8a34227c4..1b2c1e5b8f 100644 --- a/src/components/src/side-panel/panel-header.tsx +++ b/src/components/src/side-panel/panel-header.tsx @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import React, {Component} from 'react'; +import React, {Component, useCallback} from 'react'; import styled from 'styled-components'; import classnames from 'classnames'; import {createSelector} from 'reselect'; @@ -45,12 +45,12 @@ type ActionItem = { iconComponent: React.ComponentType>; iconComponentProps?: BaseProps; dropdownComponent?: React.ComponentType; - onClick?: (p: PanelHeaderProps) => void; + onClick?: () => void; }; type PanelActionProps = { item: ActionItem; - onClick: () => void; + showExportDropdown: (string) => void; }; type PanelHeaderDropdownProps = { @@ -155,19 +155,31 @@ const StyledToolbar = styled(Toolbar)` position: absolute; `; -export const PanelAction: React.FC = ({item, onClick}) => ( - - {item.label ?

{item.label}

: null} - - - - {item.tooltip ? ( - - - - ) : null} -
-); +const PanelAction: React.FC = React.memo(({item, showExportDropdown}) => { + const onClick = useCallback(() => { + if (item.dropdownComponent) { + showExportDropdown(item.id); + } else { + item.onClick && item.onClick(); + } + }, [item, showExportDropdown]); + + return ( + + {item.label ?

{item.label}

: null} + + + + {item.tooltip ? ( + + + + ) : null} +
+ ); +}); +PanelAction.displayName = 'PanelAction'; +export {PanelAction}; export const PanelHeaderDropdownFactory = () => { const PanelHeaderDropdown: React.FC = ({items, show, onClose, id}) => { @@ -356,16 +368,7 @@ function PanelHeaderFactory( key={item.id} style={{position: 'relative'}} > - { - if (item.dropdownComponent) { - showExportDropdown(item.id); - } else { - item.onClick && item.onClick(this.props); - } - }} - /> + {item.dropdownComponent ? (