fix: prevent unnecessary UI re-renders#3439
Draft
igorDykhta wants to merge 3 commits into
Draft
Conversation
Collaborator
igorDykhta
commented
May 16, 2026
- for [Bug] Any state changes force React rerender all UI #2626
- prevent unnecessary UI re-renders on 1) animation update 2) mouse move
- instead of stale non-functional fix: prevent unnecessary re-renders caused by Container subscribing to entire Redux state #3318
added 3 commits
May 16, 2026 02:36
Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>
Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>
Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR targets issue #2626 by reducing unnecessary React re-renders in kepler.gl’s UI during high-frequency updates (e.g., animation ticks and mouse move) via memoization and more stable prop selection.
Changes:
- Replaced several plain “selector functions” in
kepler-gl.tsxwithreselectmemoized selectors to stabilize derived prop objects. - Added
React.memo(with custom equality functions in some cases) to key UI components (SidePanel,MapControl,BottomWidget, etc.) to avoid re-render cascades. - Customized
react-reduxconnectbehavior incontainer.tsxusingareStatesEqualto prevent updates when the current kepler.gl instance slice is unchanged.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/components/src/side-panel.tsx | Wraps SidePanel in React.memo with a custom prop comparator to reduce re-renders during animation/state churn. |
| src/components/src/map/map-control.tsx | Wraps MapControl in React.memo with a custom comparator for layers/datasets-related props. |
| src/components/src/map-container.tsx | Memoizes Attribution / AttributionLogos components to reduce repeated renders. |
| src/components/src/loading-indicator.tsx | Memoizes LoadingIndicator before applying withTheme. |
| src/components/src/kepler-gl.tsx | Uses reselect to memoize side panel, bottom widget, and notification selectors. |
| src/components/src/container.tsx | Adds connect option areStatesEqual to avoid container updates when instance state ref is unchanged. |
| src/components/src/bottom-widget.tsx | Memoizes BottomWidget and wraps it with forwardRef + withTheme. |
Comments suppressed due to low confidence (3)
src/components/src/map-container.tsx:252
Attributionis now wrapped inReact.memo, but it derivesisPalmfromhasMobileWidth(breakPointValues)(amatchMediaquery) rather than from props/state. With memoization, the component won’t re-render when the media query result changes (e.g. window resize), so the attribution layout can get stuck in the wrong mobile/desktop mode. Consider removingReact.memohere, or subscribing to the media query (e.g. via a hook/useSyncExternalStore) and/or passing a size breakpoint value as a prop so resizes trigger a re-render.
export const Attribution: React.FC<AttributionProps> = React.memo(({
showBaseMapLibLogo = true,
showOsmBasemapAttribution = false,
datasetAttributions,
baseMapLibraryConfig
}: AttributionProps) => {
const isPalm = hasMobileWidth(breakPointValues);
src/components/src/side-panel.tsx:312
- In the
layersbranch ofareSidePanelPropsEqual, when the layer panel is open and bothlayersandfilterschange in the same update, the early-return is skipped and the comparison falls back to only checkinglayer.id/layer.type. That can treat materially changed layer configs as “equal” and prevent the Layer panel UI from updating. If the intent is to suppress animation-driven churn, consider gating on a more explicit signal (e.g. animation action/flag) or expanding the comparison for the layer panel open case to include the fields that drive the layer list UI.
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;
src/components/src/map/map-control.tsx:205
- After wrapping
MapControlwithmemo, React DevTools will typically show it asMemo(MapControl). If you want to preserve the original component name for debugging (similar to what was done forSidePanel), setMemoizedMapControl.displayName = 'MapControl'.
const MemoizedMapControl = memo(MapControl, areMapControlPropsEqual) as React.NamedExoticComponent<MapControlProps> & {
defaultActionComponents: MapControlProps['actionComponents'];
};
(MemoizedMapControl as any).defaultActionComponents = DEFAULT_ACTIONS;
return MemoizedMapControl;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+253
to
+254
| const keys = Object.keys(next) as (keyof SidePanelProps)[]; | ||
| for (const key of keys) { |
|
|
||
| return MapControl; | ||
| const areMapControlPropsEqual = (prev: MapControlProps, next: MapControlProps): boolean => { | ||
| const keys = Object.keys(next) as (keyof MapControlProps)[]; |
ilyabo
reviewed
May 18, 2026
| return MapControl; | ||
| const areMapControlPropsEqual = (prev: MapControlProps, next: MapControlProps): boolean => { | ||
| const keys = Object.keys(next) as (keyof MapControlProps)[]; | ||
| for (const key of keys) { |
Collaborator
There was a problem hiding this comment.
wouldn't some deepEqual simplify this?
ilyabo
approved these changes
May 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.