Skip to content

Commit

Permalink
[Dashboard] [Collapsable Panels] Reduce re-renders (#197343)
Browse files Browse the repository at this point in the history
Closes #191131

## Summary

This PR greatly reduces the number of React re-renders that happen as
panels get dragged around and/or resized. Now, the actions that trigger
a panel to get rendered are as follows:
1. Obviously, when the grid first loads, every panel has to be rendered.
2. When a panel gets dragged from one row to the next, both the original
row and the new row will re-render all of their panels because the panel
IDs in both rows changed - however, because of the `key` prop on the
`GridPanel` component, only the **dragged** panel will actually be fully
re-mounted.
3. When a panel gets collapsed and expanded, all panels in that row will
get re-mounted and rendered.
4. When a panel ID gets changed (this currently isn't possible, but in
theory, this would also trigger the panel to get re-rendered due to the
`key` prop on the `GridPanel` component)

In order to accomplish this, we are now handling **all style changes**
via a subscription rather than setting the CSS directly; so, as the
`gridLayout$` behaviour subjects publishes changes, we update the row +
panel styles via the panel reference. This allows us to change how the
grid looks without triggering React to rerender the entire panel.

**How to Test:**
Add a `console.log` to the `renderPanelContents` in
`examples/grid_example/public/app.tsx` - this will tell you when a panel
is getting re-rendered.

### Checklist

- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)



### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#_add_your_labels)
- [ ] This will appear in the **Release Notes** and follow the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
Heenawter and kibanamachine authored Oct 29, 2024
1 parent 641d915 commit b91fa56
Show file tree
Hide file tree
Showing 7 changed files with 415 additions and 291 deletions.
71 changes: 71 additions & 0 deletions packages/kbn-grid-layout/grid/drag_preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React, { useEffect, useRef } from 'react';
import { combineLatest, skip } from 'rxjs';

import { transparentize } from '@elastic/eui';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';

import { GridLayoutStateManager } from './types';

export const DragPreview = ({
rowIndex,
gridLayoutStateManager,
}: {
rowIndex: number;
gridLayoutStateManager: GridLayoutStateManager;
}) => {
const dragPreviewRef = useRef<HTMLDivElement | null>(null);

useEffect(
() => {
/** Update the styles of the drag preview via a subscription to prevent re-renders */
const styleSubscription = combineLatest([
gridLayoutStateManager.activePanel$,
gridLayoutStateManager.gridLayout$,
])
.pipe(skip(1)) // skip the first emit because the drag preview is only rendered after a user action
.subscribe(([activePanel, gridLayout]) => {
if (!dragPreviewRef.current) return;

if (!activePanel || !gridLayout[rowIndex].panels[activePanel.id]) {
dragPreviewRef.current.style.display = 'none';
} else {
const panel = gridLayout[rowIndex].panels[activePanel.id];
dragPreviewRef.current.style.display = 'block';
dragPreviewRef.current.style.gridColumnStart = `${panel.column + 1}`;
dragPreviewRef.current.style.gridColumnEnd = `${panel.column + 1 + panel.width}`;
dragPreviewRef.current.style.gridRowStart = `${panel.row + 1}`;
dragPreviewRef.current.style.gridRowEnd = `${panel.row + 1 + panel.height}`;
}
});

return () => {
styleSubscription.unsubscribe();
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);

return (
<div
ref={dragPreviewRef}
css={css`
display: none;
pointer-events: none;
border-radius: ${euiThemeVars.euiBorderRadius};
background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.2)};
transition: opacity 100ms linear;
`}
/>
);
};
37 changes: 25 additions & 12 deletions packages/kbn-grid-layout/grid/grid_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React from 'react';

import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import React, { useEffect, useState } from 'react';
import { distinctUntilChanged, map, skip } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';

import { GridHeightSmoother } from './grid_height_smoother';
import { GridRow } from './grid_row';
Expand All @@ -29,12 +29,28 @@ export const GridLayout = ({
});
useGridLayoutEvents({ gridLayoutStateManager });

const [gridLayout, runtimeSettings, interactionEvent] = useBatchedPublishingSubjects(
gridLayoutStateManager.gridLayout$,
gridLayoutStateManager.runtimeSettings$,
gridLayoutStateManager.interactionEvent$
const [rowCount, setRowCount] = useState<number>(
gridLayoutStateManager.gridLayout$.getValue().length
);

useEffect(() => {
/**
* The only thing that should cause the entire layout to re-render is adding a new row;
* this subscription ensures this by updating the `rowCount` state when it changes.
*/
const rowCountSubscription = gridLayoutStateManager.gridLayout$
.pipe(
skip(1), // we initialized `rowCount` above, so skip the initial emit
map((newLayout) => newLayout.length),
distinctUntilChanged()
)
.subscribe((newRowCount) => {
setRowCount(newRowCount);
});
return () => rowCountSubscription.unsubscribe();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<>
<GridHeightSmoother gridLayoutStateManager={gridLayoutStateManager}>
Expand All @@ -43,15 +59,12 @@ export const GridLayout = ({
setDimensionsRef(divElement);
}}
>
{gridLayout.map((rowData, rowIndex) => {
{Array.from({ length: rowCount }, (_, rowIndex) => {
return (
<GridRow
rowData={rowData}
key={rowData.title}
key={uuidv4()}
rowIndex={rowIndex}
runtimeSettings={runtimeSettings}
renderPanelContents={renderPanelContents}
targetRowIndex={interactionEvent?.targetRowIndex}
gridLayoutStateManager={gridLayoutStateManager}
toggleIsCollapsed={() => {
const currentLayout = gridLayoutStateManager.gridLayout$.value;
Expand Down
Loading

0 comments on commit b91fa56

Please sign in to comment.