Combobox: Refactor sortByGroup for performance (#102664)
* first pass at improving perf of sortByGroup * polish up
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
import { useOptions } from './useOptions';
|
import { sortByGroup, useOptions } from './useOptions';
|
||||||
|
|
||||||
describe('useOptions', () => {
|
describe('useOptions', () => {
|
||||||
it('should handle a large number of synchronous options without throwing an error', () => {
|
it('should handle a large number of synchronous options without throwing an error', () => {
|
||||||
@@ -101,3 +101,56 @@ describe('useOptions', () => {
|
|||||||
expect(result.current.asyncError).toBe(true);
|
expect(result.current.asyncError).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('sortByGroup', () => {
|
||||||
|
it('should return original array when no groups exist', () => {
|
||||||
|
const options = [
|
||||||
|
{ label: 'Apple', value: 'apple' },
|
||||||
|
{ label: 'Banana', value: 'banana' },
|
||||||
|
{ label: 'Carrot', value: 'carrot' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { options: sortedOptions, groupStartIndices } = sortByGroup(options);
|
||||||
|
|
||||||
|
expect(sortedOptions).toBe(options); // Check reference equality
|
||||||
|
expect(groupStartIndices.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return original array when only one group exists', () => {
|
||||||
|
const options = [
|
||||||
|
{ label: 'Apple', value: 'apple', group: 'fruits' },
|
||||||
|
{ label: 'Banana', value: 'banana', group: 'fruits' },
|
||||||
|
{ label: 'Tomato', value: 'tomato', group: 'fruits' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { options: sortedOptions, groupStartIndices } = sortByGroup(options);
|
||||||
|
|
||||||
|
expect(sortedOptions).toEqual(options);
|
||||||
|
expect(groupStartIndices.size).toBe(1);
|
||||||
|
expect(groupStartIndices.get('fruits')).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should group options and track group start indices', () => {
|
||||||
|
const options = [
|
||||||
|
{ label: 'Apple', value: 'apple', group: 'fruits' },
|
||||||
|
{ label: 'Carrot', value: 'carrot', group: 'vegetables' },
|
||||||
|
{ label: 'Banana', value: 'banana', group: 'fruits' },
|
||||||
|
{ label: 'Celery', value: 'celery', group: 'vegetables' },
|
||||||
|
{ label: 'Other', value: 'other' }, // Ungrouped
|
||||||
|
];
|
||||||
|
|
||||||
|
const { options: sortedOptions, groupStartIndices } = sortByGroup(options);
|
||||||
|
|
||||||
|
expect(sortedOptions).toEqual([
|
||||||
|
{ label: 'Apple', value: 'apple', group: 'fruits' },
|
||||||
|
{ label: 'Banana', value: 'banana', group: 'fruits' },
|
||||||
|
{ label: 'Carrot', value: 'carrot', group: 'vegetables' },
|
||||||
|
{ label: 'Celery', value: 'celery', group: 'vegetables' },
|
||||||
|
{ label: 'Other', value: 'other' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(groupStartIndices.size).toBe(2);
|
||||||
|
expect(groupStartIndices.get('fruits')).toBe(0);
|
||||||
|
expect(groupStartIndices.get('vegetables')).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -115,41 +115,61 @@ export function useOptions<T extends string | number>(rawOptions: AsyncOptions<T
|
|||||||
return { options: finalOptions, groupStartIndices, updateOptions, asyncLoading, asyncError };
|
return { options: finalOptions, groupStartIndices, updateOptions, asyncLoading, asyncError };
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortByGroup<T extends string | number>(options: Array<ComboboxOption<T>>) {
|
/**
|
||||||
|
* Sorts options by group and returns the sorted options and the starting index of each group
|
||||||
|
*/
|
||||||
|
export function sortByGroup<T extends string | number>(options: Array<ComboboxOption<T>>) {
|
||||||
|
// Group options by their group
|
||||||
const groupedOptions = new Map<string | undefined, Array<ComboboxOption<T>>>();
|
const groupedOptions = new Map<string | undefined, Array<ComboboxOption<T>>>();
|
||||||
|
const groupStartIndices = new Map<string | undefined, number>();
|
||||||
|
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
const groupExists = groupedOptions.has(option.group);
|
const group = option.group;
|
||||||
if (groupExists) {
|
const existing = groupedOptions.get(group);
|
||||||
groupedOptions.get(option.group)?.push(option);
|
if (existing) {
|
||||||
|
existing.push(option);
|
||||||
} else {
|
} else {
|
||||||
groupedOptions.set(option.group, [option]);
|
groupedOptions.set(group, [option]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a map to track the starting index of each group
|
// If we only have one group (either the undefined group, or a single group), return the original array
|
||||||
const groupStartIndices = new Map<string, number>();
|
if (groupedOptions.size <= 1) {
|
||||||
|
if (options[0]?.group) {
|
||||||
|
groupStartIndices.set(options[0]?.group, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
groupStartIndices,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'Preallocate' result array with same size as input - very minor optimization
|
||||||
|
const result: Array<ComboboxOption<T>> = new Array(options.length);
|
||||||
|
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
|
|
||||||
// Reorganize options to have groups first, then undefined group
|
// Fill result array with grouped options
|
||||||
let reorganizeOptions: Array<ComboboxOption<T>> = [];
|
|
||||||
for (const [group, groupOptions] of groupedOptions) {
|
for (const [group, groupOptions] of groupedOptions) {
|
||||||
if (!group) {
|
if (group) {
|
||||||
continue;
|
groupStartIndices.set(group, currentIndex);
|
||||||
|
for (const option of groupOptions) {
|
||||||
|
result[currentIndex++] = option;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groupStartIndices.set(group, currentIndex);
|
|
||||||
reorganizeOptions = reorganizeOptions.concat(groupOptions);
|
|
||||||
currentIndex += groupOptions.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const undefinedGroupOptions = groupedOptions.get(undefined);
|
// Add ungrouped options at the end
|
||||||
if (undefinedGroupOptions) {
|
const ungrouped = groupedOptions.get(undefined);
|
||||||
groupStartIndices.set('undefined', currentIndex);
|
if (ungrouped) {
|
||||||
reorganizeOptions = reorganizeOptions.concat(undefinedGroupOptions);
|
for (const option of ungrouped) {
|
||||||
|
result[currentIndex++] = option;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
options: reorganizeOptions,
|
options: result,
|
||||||
groupStartIndices,
|
groupStartIndices,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user