diff --git a/src/components/src/bottom-widget.tsx b/src/components/src/bottom-widget.tsx index db23757e0c..e27fc26dc3 100644 --- a/src/components/src/bottom-widget.tsx +++ b/src/components/src/bottom-widget.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import React, {forwardRef, useMemo, useCallback} from 'react'; +import React, {forwardRef, memo, useMemo, useCallback} from 'react'; import styled, {withTheme, IStyledComponent} from 'styled-components'; import {FILTER_VIEW_TYPES, EXPORT_VIDEO_ID} from '@kepler.gl/constants'; @@ -232,10 +232,15 @@ export default function BottomWidgetFactory( ); }; - return withTheme( - forwardRef((props: BottomWidgetThemedProps, ref: React.ForwardedRef) => ( - - )) + const MemoizedBottomWidget = memo(BottomWidget); + + const ForwardedBottomWidget = forwardRef( + (props: BottomWidgetThemedProps, ref: React.ForwardedRef) => ( + + ) ); + ForwardedBottomWidget.displayName = 'BottomWidget'; + + return withTheme(ForwardedBottomWidget); } /* eslint-enable complexity */ diff --git a/src/components/src/container.tsx b/src/components/src/container.tsx index dafa6a9bc3..b94ffef6c5 100644 --- a/src/components/src/container.tsx +++ b/src/components/src/container.tsx @@ -20,7 +20,18 @@ export const ERROR_MSG = { const mapStateToProps = (state: any, props: ContainerProps) => ({state, ...props}); const dispatchToProps = (dispatch: Dispatch) => ({dispatch}); -const connector = connect(mapStateToProps, dispatchToProps); +const connector = connect(mapStateToProps, dispatchToProps, null, { + areStatesEqual: (next: any, prev: any, nextOwnProps: ContainerProps) => { + const getState = nextOwnProps.getState || ((s: any) => s.keplerGl); + const id = nextOwnProps.id || 'map'; + const nextInstance = getState(next)?.[id]; + const prevInstance = getState(prev)?.[id]; + if (!prevInstance && !nextInstance) { + return next === prev; + } + return nextInstance === prevInstance; + } +}); type ContainerProps = { id: string; diff --git a/src/components/src/kepler-gl.tsx b/src/components/src/kepler-gl.tsx index 281fa64d74..7121399da0 100644 --- a/src/components/src/kepler-gl.tsx +++ b/src/components/src/kepler-gl.tsx @@ -207,32 +207,79 @@ export function getVisibleDatasets(datasets) { return filterObjectByPredicate(datasets, key => key !== GEOCODER_DATASET_NAME); } -export const sidePanelSelector = (props: KeplerGLProps, availableProviders, filteredDatasets) => ({ - appName: props.appName ? props.appName : DEFAULT_KEPLER_GL_PROPS.appName, - version: props.version ? props.version : DEFAULT_KEPLER_GL_PROPS.version, - appWebsite: props.appWebsite, - mapStyle: props.mapStyle, - onSaveMap: props.onSaveMap, - uiState: props.uiState, - mapStyleActions: props.mapStyleActions, - visStateActions: props.visStateActions, - uiStateActions: props.uiStateActions, - mapStateActions: props.mapStateActions, - - datasets: filteredDatasets, - filters: props.visState.filters, - layers: props.visState.layers, - layerOrder: props.visState.layerOrder, - layerClasses: props.visState.layerClasses, - interactionConfig: props.visState.interactionConfig, - mapInfo: props.visState.mapInfo, - layerBlending: props.visState.layerBlending, - overlayBlending: props.visState.overlayBlending, - - width: props.sidePanelWidth ? props.sidePanelWidth : DEFAULT_KEPLER_GL_PROPS.width, - availableProviders, - mapSaved: props.providerState.mapSaved -}); +export const sidePanelSelector = createSelector( + [ + (props: KeplerGLProps) => props.appName, + (props: KeplerGLProps) => props.version, + (props: KeplerGLProps) => props.appWebsite, + (props: KeplerGLProps) => props.mapStyle, + (props: KeplerGLProps) => props.onSaveMap, + (props: KeplerGLProps) => props.uiState, + (props: KeplerGLProps) => props.mapStyleActions, + (props: KeplerGLProps) => props.visStateActions, + (props: KeplerGLProps) => props.uiStateActions, + (props: KeplerGLProps) => props.mapStateActions, + (props: KeplerGLProps) => props.visState.filters, + (props: KeplerGLProps) => props.visState.layers, + (props: KeplerGLProps) => props.visState.layerOrder, + (props: KeplerGLProps) => props.visState.layerClasses, + (props: KeplerGLProps) => props.visState.interactionConfig, + (props: KeplerGLProps) => props.visState.mapInfo, + (props: KeplerGLProps) => props.visState.layerBlending, + (props: KeplerGLProps) => props.visState.overlayBlending, + (props: KeplerGLProps) => props.sidePanelWidth, + (props: KeplerGLProps) => props.providerState.mapSaved, + (_props: KeplerGLProps, availableProviders) => availableProviders, + (_props: KeplerGLProps, _availableProviders, filteredDatasets) => filteredDatasets + ], + ( + appName, + version, + appWebsite, + mapStyle, + onSaveMap, + uiState, + mapStyleActions, + visStateActions, + uiStateActions, + mapStateActions, + filters, + layers, + layerOrder, + layerClasses, + interactionConfig, + mapInfo, + layerBlending, + overlayBlending, + sidePanelWidth, + mapSaved, + availableProviders, + filteredDatasets + ) => ({ + appName: appName ? appName : DEFAULT_KEPLER_GL_PROPS.appName, + version: version ? version : DEFAULT_KEPLER_GL_PROPS.version, + appWebsite, + mapStyle, + onSaveMap, + uiState, + mapStyleActions, + visStateActions, + uiStateActions, + mapStateActions, + datasets: filteredDatasets, + filters, + layers, + layerOrder, + layerClasses, + interactionConfig, + mapInfo, + layerBlending, + overlayBlending, + width: sidePanelWidth ? sidePanelWidth : DEFAULT_KEPLER_GL_PROPS.width, + availableProviders, + mapSaved + }) +); export const plotContainerSelector = (props: KeplerGLProps) => ({ width: props.width, @@ -255,16 +302,41 @@ export const plotContainerSelector = (props: KeplerGLProps) => ({ export const isSplitSelector = (props: KeplerGLProps) => props.visState.splitMaps && props.visState.splitMaps.length > 1; -export const bottomWidgetSelector = (props: KeplerGLProps, theme) => ({ - filters: props.visState.filters, - datasets: props.visState.datasets, - uiState: props.uiState, - layers: props.visState.layers, - animationConfig: props.visState.animationConfig, - visStateActions: props.visStateActions, - toggleModal: props.uiStateActions.toggleModal, - sidePanelWidth: props.uiState.readOnly ? 0 : props.sidePanelWidth + theme.sidePanel.margin.left -}); +export const bottomWidgetSelector = createSelector( + [ + (props: KeplerGLProps) => props.visState.filters, + (props: KeplerGLProps) => props.visState.datasets, + (props: KeplerGLProps) => props.uiState, + (props: KeplerGLProps) => props.visState.layers, + (props: KeplerGLProps) => props.visState.animationConfig, + (props: KeplerGLProps) => props.visStateActions, + (props: KeplerGLProps) => props.uiStateActions.toggleModal, + (props: KeplerGLProps) => props.uiState.readOnly, + (props: KeplerGLProps) => props.sidePanelWidth, + (_props: KeplerGLProps, theme) => theme + ], + ( + filters, + datasets, + uiState, + layers, + animationConfig, + visStateActions, + toggleModal, + readOnly, + sidePanelWidth, + theme + ) => ({ + filters, + datasets, + uiState, + layers, + animationConfig, + visStateActions, + toggleModal, + sidePanelWidth: readOnly ? 0 : sidePanelWidth + theme.sidePanel.margin.left + }) +); export const modalContainerSelector = (props: KeplerGLProps, rootNode) => ({ appName: props.appName ? props.appName : DEFAULT_KEPLER_GL_PROPS.appName, @@ -413,10 +485,16 @@ export const attributionSelector = createSelector( } ); -export const notificationPanelSelector = (props: KeplerGLProps) => ({ - removeNotification: props.uiStateActions.removeNotification, - notifications: props.uiState.notifications -}); +export const notificationPanelSelector = createSelector( + [ + (props: KeplerGLProps) => props.uiStateActions.removeNotification, + (props: KeplerGLProps) => props.uiState.notifications + ], + (removeNotification, notifications) => ({ + removeNotification, + notifications + }) +); export const DEFAULT_KEPLER_GL_PROPS = { mapStyles: [], diff --git a/src/components/src/loading-indicator.tsx b/src/components/src/loading-indicator.tsx index 056f1a223e..2fec1f9b81 100644 --- a/src/components/src/loading-indicator.tsx +++ b/src/components/src/loading-indicator.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import React, {PropsWithChildren, useRef, useEffect} from 'react'; +import React, {PropsWithChildren, useRef, useEffect, memo} from 'react'; import styled, {withTheme, keyframes} from 'styled-components'; import {getNumRasterTilesBeingLoaded, getNumVectorTilesBeingLoaded} from '@kepler.gl/layers'; @@ -109,4 +109,6 @@ const LoadingIndicator: React.FC = ({ ); }; -export default withTheme(LoadingIndicator) as React.FC>; +const MemoizedLoadingIndicator = memo(LoadingIndicator); + +export default withTheme(MemoizedLoadingIndicator) as React.FC>; diff --git a/src/components/src/map-container.tsx b/src/components/src/map-container.tsx index 09b7b3e43f..b79bf5abc2 100644 --- a/src/components/src/map-container.tsx +++ b/src/components/src/map-container.tsx @@ -243,7 +243,7 @@ type AttributionProps = { baseMapLibraryConfig: BaseMapLibraryConfig; }; -export const Attribution: React.FC = ({ +export const Attribution: React.FC = React.memo(({ showBaseMapLibLogo = true, showOsmBasemapAttribution = false, datasetAttributions, @@ -311,7 +311,7 @@ export const Attribution: React.FC = ({ ]); return memoizedComponents; -}; +}); const StyledAttributionLogoContainer = styled.div<{$left: number}>` position: absolute; @@ -339,7 +339,7 @@ type AttributionLogosProps = { const LOGO_LEFT_ADJUSTMENT = 3; -export const AttributionLogos: React.FC = ({ +export const AttributionLogos: React.FC = React.memo(({ logos, activeSidePanel, sidePanelWidth @@ -365,7 +365,7 @@ export const AttributionLogos: React.FC = ({ ))} ); -}; +}); MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory]; diff --git a/src/components/src/map/map-control.tsx b/src/components/src/map/map-control.tsx index 1802a30e44..67736db29d 100644 --- a/src/components/src/map/map-control.tsx +++ b/src/components/src/map/map-control.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import React from 'react'; +import React, {memo} from 'react'; import styled from 'styled-components'; import KeplerGlLogo from '../common/logo'; @@ -143,7 +143,66 @@ function MapControlFactory( MapControl.displayName = 'MapControl'; - return MapControl; + const areMapControlPropsEqual = (prev: MapControlProps, next: MapControlProps): boolean => { + const keys = Object.keys(next) as (keyof MapControlProps)[]; + for (const key of keys) { + if (prev[key] === next[key]) continue; + + if (key === 'layers') { + const pl = prev.layers; + const nl = next.layers; + if (!pl || !nl || pl.length !== nl.length) return false; + for (let i = 0; i < nl.length; i++) { + if (pl[i] === nl[i]) continue; + if (pl[i].id !== nl[i].id) return false; + if (pl[i].config.isVisible !== nl[i].config.isVisible) return false; + if (pl[i].config.label !== nl[i].config.label) return false; + if (pl[i].config.isConfigActive !== nl[i].config.isConfigActive) return false; + } + continue; + } + + if (key === 'datasets') { + const pd = prev.datasets; + const nd = next.datasets; + if (!pd || !nd) return false; + const pKeys = Object.keys(pd); + const nKeys = Object.keys(nd); + if (pKeys.length !== nKeys.length) return false; + for (const dk of nKeys) { + if (!pd[dk]) return false; + if (pd[dk] === nd[dk]) continue; + if (pd[dk].id !== nd[dk].id) return false; + if (pd[dk].label !== nd[dk].label) return false; + if (pd[dk].color !== nd[dk].color) return false; + } + continue; + } + + if (key === 'layersToRender') { + const pl = prev.layersToRender; + const nl = next.layersToRender; + if (!pl || !nl) return false; + const pKeys = Object.keys(pl); + const nKeys = Object.keys(nl); + if (pKeys.length !== nKeys.length) return false; + for (const lk of nKeys) { + if (pl[lk] !== nl[lk]) return false; + } + continue; + } + + return false; + } + return true; + }; + + const MemoizedMapControl = memo(MapControl, areMapControlPropsEqual) as React.NamedExoticComponent & { + defaultActionComponents: MapControlProps['actionComponents']; + }; + (MemoizedMapControl as any).defaultActionComponents = DEFAULT_ACTIONS; + + return MemoizedMapControl; } export default MapControlFactory; diff --git a/src/components/src/side-panel.tsx b/src/components/src/side-panel.tsx index ea616c0105..075b96d75c 100644 --- a/src/components/src/side-panel.tsx +++ b/src/components/src/side-panel.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import React, {useCallback, useMemo} from 'react'; +import React, {memo, useCallback, useMemo} from 'react'; import { EXPORT_DATA_ID, @@ -248,5 +248,79 @@ export default function SidePanelFactory( }; SidePanel.defaultPanels = fullPanels; - return SidePanel; + + const areSidePanelPropsEqual = (prev: SidePanelProps, next: SidePanelProps): boolean => { + const keys = Object.keys(next) as (keyof SidePanelProps)[]; + for (const key of keys) { + if (prev[key] === next[key]) continue; + + if (key === 'filters') { + const pf = prev.filters; + const nf = next.filters; + if (pf?.length !== nf?.length) return false; + const isFilterPanelOpen = (next as any).uiState?.activeSidePanel === 'filter'; + for (let i = 0; i < nf.length; i++) { + if (pf[i] === nf[i]) continue; + if (pf[i].id !== nf[i].id) return false; + if (pf[i].name !== nf[i].name) return false; + if (pf[i].type !== nf[i].type) return false; + if (pf[i].dataId !== nf[i].dataId) return false; + if (pf[i].view !== nf[i].view) return false; + if (pf[i].enabled !== nf[i].enabled) return false; + if (pf[i].plotType !== nf[i].plotType) return false; + if ((pf[i] as any).animationWindow !== (nf[i] as any).animationWindow) return false; + if (pf[i].speed !== nf[i].speed) return false; + if (pf[i].gpu !== nf[i].gpu) return false; + if (isFilterPanelOpen && nf[i].view !== 'enlarged') { + if (pf[i].value !== nf[i].value) return false; + } + } + continue; + } + + if (key === 'datasets') { + const pd = prev.datasets; + const nd = next.datasets; + const pKeys = Object.keys(pd || {}); + const nKeys = Object.keys(nd || {}); + if (pKeys.length !== nKeys.length) return false; + for (const dk of nKeys) { + if (!pd?.[dk]) return false; + if (pd[dk] === nd[dk]) continue; + if (pd[dk].id !== nd[dk].id) return false; + if (pd[dk].label !== nd[dk].label) return false; + if (pd[dk].color !== nd[dk].color) return false; + if (pd[dk].fields !== nd[dk].fields) return false; + if (pd[dk].dataContainer !== nd[dk].dataContainer) return false; + } + continue; + } + + if (key === 'layers') { + const isLayerPanelOpen = (next as any).uiState?.activeSidePanel === 'layer'; + if (isLayerPanelOpen) { + const filtersAlsoChanged = prev.filters !== next.filters; + if (!filtersAlsoChanged) return false; + } + const pl = prev.layers; + const nl = next.layers; + if (pl?.length !== nl?.length) return false; + for (let i = 0; i < nl.length; i++) { + if (pl[i].id !== nl[i].id) return false; + if (pl[i].type !== nl[i].type) return false; + } + continue; + } + + return false; + } + return true; + }; + + const MemoizedSidePanel = memo(SidePanel, areSidePanelPropsEqual) as React.NamedExoticComponent & { + defaultPanels: SidePanelProps['panels']; + }; + MemoizedSidePanel.displayName = 'SidePanel'; + (MemoizedSidePanel as any).defaultPanels = fullPanels; + return MemoizedSidePanel; }