-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uxd 1984 Data Table updates on data change #1233
Changes from all commits
b383d5d
fe91a9a
e2c4eeb
21af5cb
306a9d8
4b1529b
c317a2a
f2f1efd
875d358
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@paprika/data-table": minor | ||
--- | ||
|
||
DataTable updates on data change |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import React from "react"; | ||
import { ListChildComponentProps, VariableSizeList } from "react-window"; | ||
import isEqual from "lodash.isequal"; | ||
import ReactInfiniteLoader from "react-window-infinite-loader"; | ||
import TableRow from "../TableRow/TableRow"; | ||
import { TableDataItemType } from "../../types"; | ||
|
@@ -39,6 +40,7 @@ interface InfiniteLoaderPrivateProps { | |
height: number; | ||
getRowHeight: ((index: number) => number) | null; | ||
shouldHaveHorizontalScroll: boolean; | ||
resetDimension: () => void; | ||
} | ||
|
||
function Row(props: ListChildComponentProps): JSX.Element { | ||
|
@@ -63,12 +65,36 @@ export function InfiniteLoaderImpl({ | |
isNextPageLoading = false, | ||
minimumBatchSize = 10, | ||
threshold = 15, | ||
resetDimension, | ||
}: InfiniteLoaderPrivateProps & InfiniteLoaderPublicProps): JSX.Element { | ||
const infiniteLoaderRef = React.useRef(null); | ||
const listRef = React.useRef<VariableSizeList>(null); | ||
const { getItemSize } = useItemSizeCalculator(data, getRowHeight); | ||
const isLoadingMoreItemsRef = React.useRef(false); | ||
const prevData = React.useRef<TableDataItemType[]>(data); | ||
const { getItemSize, clearRowHeights } = useItemSizeCalculator(data, getRowHeight); | ||
|
||
React.useLayoutEffect(() => { | ||
if (!isLoadingMoreItemsRef.current && listRef.current) { | ||
const changedIndexes: number[] = []; | ||
prevData.current.forEach((row: TableDataItemType, index: number) => { | ||
if (!isEqual(row, data[index])) { | ||
changedIndexes.push(index); | ||
} | ||
}); | ||
if (changedIndexes.length > 0) { | ||
clearRowHeights(changedIndexes); | ||
listRef.current.resetAfterIndex(changedIndexes[0]); | ||
setTimeout(resetDimension, 0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Finding i've had to use setTimeout here to ensure the resize gets applied after the react table has finished resetting, I don't see a way to avoid it |
||
} else if (data.length !== prevData.current.length) { | ||
resetDimension(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Originally I only call this if the |
||
} | ||
} | ||
prevData.current = data; | ||
isLoadingMoreItemsRef.current = false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensuring there are not unnecessary calculations being done when additional rows are being added by using this flag when necessary |
||
}, [data, clearRowHeights, resetDimension]); | ||
|
||
async function handleLoadMoreItems() { | ||
isLoadingMoreItemsRef.current = true; | ||
await loadMoreItems(); | ||
|
||
if (listRef.current) { | ||
|
@@ -94,7 +120,9 @@ export function InfiniteLoaderImpl({ | |
onItemsRendered={onItemsRendered} | ||
ref={listRef} | ||
innerElementType={innerElementType} | ||
style={{ overflowX: shouldHaveHorizontalScroll ? "auto" : "hidden" }} | ||
style={{ | ||
overflowX: shouldHaveHorizontalScroll ? "auto" : "hidden", | ||
}} | ||
> | ||
{Row} | ||
</VariableSizeList> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ const rowHeightHelper = new RowHeightHelper(); | |
export default function useItemSizeCalculator( | ||
data: TableDataItemType[], | ||
getRowHeight: ((index: number) => number) | null | ||
): { getItemSize: (index: number) => number } { | ||
): { getItemSize: (index: number) => number; clearRowHeights: (indexes: number[]) => void } { | ||
const rowHeights = React.useRef<Record<number, number>>({}); | ||
const { allColumns } = useReactTableContext(); | ||
|
||
|
@@ -24,6 +24,11 @@ export default function useItemSizeCalculator( | |
[allColumns] | ||
); | ||
|
||
const clearRowHeights = React.useCallback( | ||
(indexes: number[]) => indexes.forEach(index => delete rowHeights.current[index]), | ||
[] | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. only re-evaluating the data row heights for rows that have changed |
||
|
||
function getItemSize(index: number): number { | ||
if (!rowHeights.current[index]) { | ||
const newRowHeight = | ||
|
@@ -44,5 +49,5 @@ export default function useItemSizeCalculator( | |
return rowHeights.current[index]; | ||
} | ||
|
||
return { getItemSize }; | ||
return { getItemSize, clearRowHeights }; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,7 +22,7 @@ export default function useBestTableDimensions({ | |
maxHeight: string; | ||
maxWidth: string; | ||
shouldResizeWithViewport: boolean; | ||
}): Dimensions { | ||
}): { dimensions: Dimensions; resetDimension: () => void } { | ||
const [dimensions, setDimensions] = React.useState<Dimensions>(() => ({ | ||
width: convertSizeStringToNumber(maxWidth, Direction.width), | ||
height: convertSizeStringToNumber(maxHeight, Direction.height), | ||
|
@@ -72,5 +72,5 @@ export default function useBestTableDimensions({ | |
resize: resetDimension, | ||
})); | ||
|
||
return dimensions; | ||
return { dimensions, resetDimension }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we're adding exports here, maybe we can move There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well the |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import React from "react"; | ||
import Input from "@paprika/input"; | ||
import Button from "@paprika/button"; | ||
import DataTable from "../../src"; | ||
import makeData from "../helpers/makeData"; | ||
|
||
const DataChangeStory: () => JSX.Element = () => { | ||
const inputRef = React.useRef<HTMLInputElement>(null); | ||
const columns = React.useMemo( | ||
() => [ | ||
{ | ||
Header: "First Name", | ||
accessor: "firstName", | ||
width: 100, | ||
}, | ||
{ | ||
Header: "Description", | ||
width: 100, | ||
accessor: "desc", | ||
}, | ||
{ | ||
Header: "Last Name", | ||
accessor: "lastName", | ||
width: 100, | ||
}, | ||
{ | ||
Header: "Age", | ||
accessor: "age", | ||
width: 50, | ||
}, | ||
{ | ||
Header: "Visits", | ||
accessor: "visits", | ||
width: 60, | ||
}, | ||
{ | ||
Header: "Status", | ||
accessor: "status", | ||
}, | ||
], | ||
[] | ||
); | ||
|
||
const [items, setItems] = React.useState(() => makeData(5).map(item => ({ ...item, desc: "a custom description" }))); | ||
|
||
const handleSave = () => { | ||
setItems(prevItems => { | ||
const newItemCopy = { ...prevItems[2] }; | ||
const inputValue = inputRef && inputRef.current ? inputRef.current.value : ""; | ||
newItemCopy.desc = inputValue; | ||
const newItems = [...prevItems]; | ||
newItems[0] = newItemCopy; | ||
newItems[2] = newItemCopy; | ||
return newItems; | ||
}); | ||
}; | ||
|
||
const handleAddItem = () => { | ||
setItems(prevItems => { | ||
const newItemCopy = { ...prevItems[0] }; | ||
const newItems = [...prevItems]; | ||
newItems.push(newItemCopy); | ||
return newItems; | ||
}); | ||
}; | ||
|
||
return ( | ||
<> | ||
<Input | ||
ref={inputRef} | ||
defaultValue="This will be the description that will be set in the data on the first and third row. Click save to update the table data" | ||
/> | ||
<Button onClick={handleSave}>Save</Button> | ||
<Button onClick={handleAddItem}>Add item</Button> | ||
<DataTable a11yText="A simple data table." columns={columns} data={items}> | ||
<DataTable.InfiniteLoader | ||
itemCount={items.length} | ||
isItemLoaded={index => items[index] !== undefined} | ||
loadMoreItems={async () => { | ||
console.log("callign loadmore items"); | ||
// do nothing | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Loading more items is kind of a data change event as well, can we enable it here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well this story is about the data |
||
}} | ||
/> | ||
</DataTable> | ||
</> | ||
); | ||
}; | ||
|
||
export default DataChangeStory; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just feel these calculations are quite expensive, e.g. the data from the
data[index]
might not always be useful. Some columns might be hidden, but the data for those columns are still in the data object passed into DataTable.Another example is, let's say only the data of row number 2 is changed, but since we have 200 rows, we still need to check if the data for row number 3 - row number 200 have been changed
So I'd still prefer using
useImperativeHandle
to let the consumer decide whichindexes
they updated, or from whichindex
the data are newly updated. Open for discussions!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, It is expensive but my thoughts are if we don't do it here we will still need to evaluate which indexes have changed in the consumer. I would have thought letting the dataTable deal with that would be easier for developers using it.