Fix redraw after resizing elements

Summary:
Fixed a longer standing issue where after a horizontal resize the rows wouldn't redraw until new data arrives (or the user scrolls), resulting in rendering artefacts.

Also introduced a hook to force a reflow of contents if the contents of a hook changes. It's a bit leaky abstraction, but does keep the virtualization performant if dynamic heights are used.

Reviewed By: passy

Differential Revision: D27395516

fbshipit-source-id: 1691af3ec64f1a476969a318553d83e22239997c
This commit is contained in:
Michel Weststrate
2021-03-31 03:42:59 -07:00
committed by Facebook GitHub Bot
parent b597da01e7
commit 8cd38a6b49
4 changed files with 92 additions and 45 deletions

View File

@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@emotion/css": "^11.1.3", "@emotion/css": "^11.1.3",
"@emotion/react": "^11.1.5", "@emotion/react": "^11.1.5",
"@reach/observe-rect": "^1.2.0",
"immer": "^9.0.0", "immer": "^9.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react-element-to-jsx-string": "^14.3.2", "react-element-to-jsx-string": "^14.3.2",

View File

@@ -15,10 +15,11 @@ import {
import {Button, Typography} from 'antd'; import {Button, Typography} from 'antd';
import {pad} from 'lodash'; import {pad} from 'lodash';
import React, {createElement, Fragment, isValidElement, useState} from 'react'; import React, {createElement, Fragment, isValidElement, useState} from 'react';
import styled from '@emotion/styled';
import {tryGetFlipperLibImplementation} from '../plugin/FlipperLib'; import {tryGetFlipperLibImplementation} from '../plugin/FlipperLib';
import {safeStringify} from '../utils/safeStringify'; import {safeStringify} from '../utils/safeStringify';
import {urlRegex} from '../utils/urlRegex'; import {urlRegex} from '../utils/urlRegex';
import {useTableRedraw} from './datatable/DataSourceRenderer';
import {theme} from './theme';
/** /**
* A Formatter is used to render an arbitrarily value to React. If a formatter returns 'undefined' * A Formatter is used to render an arbitrarily value to React. If a formatter returns 'undefined'
@@ -136,7 +137,7 @@ export const DataFormatter = {
}, },
}; };
const TruncateHelper = styled(function TruncateHelper({ function TruncateHelper({
value, value,
maxLength, maxLength,
}: { }: {
@@ -144,29 +145,37 @@ const TruncateHelper = styled(function TruncateHelper({
maxLength: number; maxLength: number;
}) { }) {
const [collapsed, setCollapsed] = useState(true); const [collapsed, setCollapsed] = useState(true);
const redrawRow = useTableRedraw();
return ( return (
<> <>
{collapsed ? value.substr(0, maxLength) : value} {collapsed ? value.substr(0, maxLength) : value}
<Button <Button
onClick={() => setCollapsed((c) => !c)} onClick={() => {
setCollapsed((c) => !c);
redrawRow?.();
}}
size="small" size="small"
type="text" type="text"
style={truncateButtonStyle}
icon={collapsed ? <CaretRightOutlined /> : <CaretUpOutlined />}> icon={collapsed ? <CaretRightOutlined /> : <CaretUpOutlined />}>
{`(and ${value.length - maxLength} more...)`} {`(and ${value.length - maxLength} more...)`}
</Button> </Button>
<Button <Button
icon={<CopyOutlined />} icon={<CopyOutlined />}
onClick={() => onClick={() => {
tryGetFlipperLibImplementation()?.writeTextToClipboard(value) tryGetFlipperLibImplementation()?.writeTextToClipboard(value);
} }}
size="small" size="small"
type="text"> type="text"
style={truncateButtonStyle}>
Copy Copy
</Button> </Button>
</> </>
); );
})({ }
'& button': {
marginRight: 4, const truncateButtonStyle = {
}, color: theme.textColorPrimary,
}); marginLeft: 4,
};

View File

@@ -15,10 +15,13 @@ import React, {
useState, useState,
useLayoutEffect, useLayoutEffect,
MutableRefObject, MutableRefObject,
useContext,
createContext,
} from 'react'; } from 'react';
import {DataSource} from '../../state/DataSource'; import {DataSource} from '../../state/DataSource';
import {useVirtual} from 'react-virtual'; import {useVirtual} from 'react-virtual';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import observeRect from '@reach/observe-rect';
// how fast we update if updates are low-prio (e.g. out of window and not super significant) // how fast we update if updates are low-prio (e.g. out of window and not super significant)
const LOW_PRIO_UPDATE = 1000; //ms const LOW_PRIO_UPDATE = 1000; //ms
@@ -92,9 +95,8 @@ export const DataSourceRenderer: <T extends object, C>(
// render scheduling // render scheduling
const renderPending = useRef(UpdatePrio.NONE); const renderPending = useRef(UpdatePrio.NONE);
const lastRender = useRef(Date.now()); const lastRender = useRef(Date.now());
const setForceUpdate = useState(0)[1]; const [, setForceUpdate] = useState(0);
const forceHeightRecalculation = useRef(0); const forceHeightRecalculation = useRef(0);
const parentRef = React.useRef<null | HTMLDivElement>(null); const parentRef = React.useRef<null | HTMLDivElement>(null);
const virtualizer = useVirtual({ const virtualizer = useVirtual({
@@ -111,6 +113,11 @@ export const DataSourceRenderer: <T extends object, C>(
virtualizerRef.current = virtualizer; virtualizerRef.current = virtualizer;
} }
const redraw = useCallback(() => {
forceHeightRecalculation.current++;
setForceUpdate((x) => x + 1);
}, []);
useEffect( useEffect(
function subscribeToDataSource() { function subscribeToDataSource() {
const forceUpdate = () => { const forceUpdate = () => {
@@ -261,40 +268,64 @@ export const DataSourceRenderer: <T extends object, C>(
lastRender.current = Date.now(); lastRender.current = Date.now();
}); });
/**
* Observer parent height
*/
useEffect(
function redrawOnResize() {
if (!parentRef.current) {
return;
}
let lastWidth = 0;
const observer = observeRect(parentRef.current, (rect) => {
if (lastWidth !== rect.width) {
lastWidth = rect.width;
redraw();
}
});
observer.observe();
return () => observer.unobserve();
},
[redraw],
);
/** /**
* Rendering * Rendering
*/ */
return ( return (
<TableContainer onScroll={onScroll} ref={parentRef}> <RedrawContext.Provider value={redraw}>
{virtualizer.virtualItems.length === 0 <TableContainer onScroll={onScroll} ref={parentRef}>
? emptyRenderer?.(dataSource) {virtualizer.virtualItems.length === 0
: null} ? emptyRenderer?.(dataSource)
<TableWindow : null}
height={virtualizer.totalSize} <TableWindow
onKeyDown={onKeyDown} height={virtualizer.totalSize}
tabIndex={0}> onKeyDown={onKeyDown}
{virtualizer.virtualItems.map((virtualRow) => { tabIndex={0}>
const value = dataSource.view.get(virtualRow.index); {virtualizer.virtualItems.map((virtualRow) => {
// the position properties always change, so they are not part of the TableRow to avoid invalidating the memoized render always. const value = dataSource.view.get(virtualRow.index);
// Also all row containers are renderd as part of same component to have 'less react' framework code in between*/} // the position properties always change, so they are not part of the TableRow to avoid invalidating the memoized render always.
return ( // Also all row containers are renderd as part of same component to have 'less react' framework code in between*/}
<div return (
key={virtualRow.index} <div
style={{ key={virtualRow.index}
position: 'absolute', style={{
top: 0, position: 'absolute',
left: 0, top: 0,
width: '100%', left: 0,
height: useFixedRowHeight ? virtualRow.size : undefined, width: '100%',
transform: `translateY(${virtualRow.start}px)`, height: useFixedRowHeight ? virtualRow.size : undefined,
}} transform: `translateY(${virtualRow.start}px)`,
ref={useFixedRowHeight ? undefined : virtualRow.measureRef}> }}
{itemRenderer(value, virtualRow.index, context)} ref={useFixedRowHeight ? undefined : virtualRow.measureRef}>
</div> {itemRenderer(value, virtualRow.index, context)}
); </div>
})} );
</TableWindow> })}
</TableContainer> </TableWindow>
</TableContainer>
</RedrawContext.Provider>
); );
}) as any; }) as any;
@@ -310,3 +341,9 @@ const TableWindow = styled.div<{height: number}>(({height}) => ({
position: 'relative', position: 'relative',
width: '100%', width: '100%',
})); }));
export const RedrawContext = createContext<undefined | (() => void)>(undefined);
export function useTableRedraw() {
return useContext(RedrawContext);
}

View File

@@ -2150,7 +2150,7 @@
resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493" resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493"
integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw== integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw==
"@reach/observe-rect@^1.1.0": "@reach/observe-rect@^1.1.0", "@reach/observe-rect@^1.2.0":
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ== integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==