Skip to content

Commit

Permalink
Further memoize sorting utils
Browse files Browse the repository at this point in the history
- use new deep equal memoization hook to reduce rerenders on consumer-passed `sorting` and `sortingColumns`

- separate out memoized `sortedRowMap` to reduce dependencies/rerenders further

- rewrite `inMemoryRowIndices` more succinctly
  • Loading branch information
kqualters-elastic authored and cee-chen committed Mar 8, 2024
1 parent 3696a75 commit 0dbd5bb
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 50 deletions.
98 changes: 50 additions & 48 deletions src/components/datagrid/utils/sorting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { createContext, useMemo, useCallback } from 'react';
import { useDeepEqual } from '../../../services';
import {
DataGridSortingContextShape,
EuiDataGridSorting,
Expand Down Expand Up @@ -41,64 +42,52 @@ export const useSorting = ({
schemaDetectors,
startRow,
}: useSortingArgs) => {
const sortingColumns = sorting?.columns;

const sortedRowMap = useMemo(() => {
const rowMap: DataGridSortingContextShape['sortedRowMap'] = [];
const memoizedSorting = useDeepEqual(sorting);
const sortingColumns = useDeepEqual(sorting?.columns);

const sortedWrappedValues = useMemo(() => {
if (
inMemory?.level === 'sorting' &&
sortingColumns != null &&
sortingColumns.length > 0
) {
const inMemoryRowIndices = Object.keys(inMemoryValues);
const wrappedValues: Array<{
index: number;
values: EuiDataGridInMemoryValues[number];
}> = [];
for (let i = 0; i < inMemoryRowIndices.length; i++) {
const inMemoryRow = inMemoryValues[inMemoryRowIndices[i]];
wrappedValues.push({ index: i, values: inMemoryRow });
}

wrappedValues.sort((a, b) => {
for (let i = 0; i < sortingColumns.length; i++) {
const column = sortingColumns[i];
const aValue = a.values[column.id];
const bValue = b.values[column.id];

// get the comparator, based on schema
let comparator = defaultComparator;
if (schema.hasOwnProperty(column.id)) {
const columnType = schema[column.id].columnType;
for (let i = 0; i < schemaDetectors.length; i++) {
const detector = schemaDetectors[i];
if (
detector.type === columnType &&
detector.hasOwnProperty('comparator')
) {
comparator = detector.comparator!;
return inMemoryRowIndices
.map((row, index) => {
return { index, values: inMemoryValues[row] };
})
.sort((a, b) => {
for (let i = 0; i < sortingColumns.length; i++) {
const column = sortingColumns[i];
const aValue = a.values[column.id];
const bValue = b.values[column.id];

// get the comparator, based on schema
let comparator = defaultComparator;
if (schema.hasOwnProperty(column.id)) {
const columnType = schema[column.id].columnType;
for (let i = 0; i < schemaDetectors.length; i++) {
const detector = schemaDetectors[i];
if (
detector.type === columnType &&
detector.hasOwnProperty('comparator')
) {
comparator = detector.comparator!;
}
}
}
}

const result = comparator(aValue, bValue, column.direction, {
aIndex: a.index,
bIndex: b.index,
});
// only return if the columns are unequal, otherwise allow the next sort-by column to run
if (result !== 0) return result;
}

return 0;
});
const result = comparator(aValue, bValue, column.direction, {
aIndex: a.index,
bIndex: b.index,
});
// only return if the columns are unequal, otherwise allow the next sort-by column to run
if (result !== 0) return result;
}

for (let i = 0; i < wrappedValues.length; i++) {
rowMap[i] = wrappedValues[i].index;
}
return 0;
});
}

return rowMap;
}, [
inMemory?.level,
inMemoryValues,
Expand All @@ -107,6 +96,19 @@ export const useSorting = ({
schemaDetectors,
]);

const sortedRowMap = useMemo(() => {
if (
inMemory?.level === 'sorting' &&
sortingColumns != null &&
sortingColumns.length > 0 &&
sortedWrappedValues != null
) {
return sortedWrappedValues.map((row) => row.index);
} else {
return [];
}
}, [inMemory?.level, sortingColumns, sortedWrappedValues]);

// Given a visible row index, obtain the unpaginated & unsorted
// row index from the passed cell data
const getCorrectRowIndex = useCallback(
Expand All @@ -128,9 +130,9 @@ export const useSorting = ({

return useMemo(() => {
return {
sorting,
sorting: memoizedSorting,
sortedRowMap,
getCorrectRowIndex,
};
}, [sorting, sortedRowMap, getCorrectRowIndex]);
}, [memoizedSorting, sortedRowMap, getCorrectRowIndex]);
};
17 changes: 17 additions & 0 deletions src/services/hooks/useDeepEqual.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,21 @@ describe('useDeepEqual', () => {

expect(first).not.toBe(second);
});

it('also works for arrays', () => {
const { result, rerender } = renderHook(useDeepEqual, {
initialProps: ['bar', 'baz'],
});
const first = result.current;

rerender(['bar', 'baz']);
const second = result.current;

expect(first).toBe(second);

rerender(['foo', 'bar', 'baz']);
const third = result.current;

expect(second).not.toBe(third);
});
});
6 changes: 4 additions & 2 deletions src/services/hooks/useDeepEqual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import { useRef } from 'react';
import isEqual from 'lodash/isEqual';

/**
* This hook is mostly a performance concern for third-party objects that EUI
* This hook is mostly a performance concern for third-party objs/arrays that EUI
* has no control over and may not be correctly memoized (i.e., will create a new
* reference on every rerender unless passed through this hook).
*/
export const useDeepEqual = <T = Record<string, any>>(object: T): T => {
export const useDeepEqual = <T = Record<string, any> | any[] | undefined>(
object: T
): T => {
const ref = useRef(object);

if (!isEqual(object, ref.current)) {
Expand Down

0 comments on commit 0dbd5bb

Please sign in to comment.