diff --git a/src/__tests__/use-collection.test.tsx b/src/__tests__/use-collection.test.tsx
index 0b02484..fdd29d9 100644
--- a/src/__tests__/use-collection.test.tsx
+++ b/src/__tests__/use-collection.test.tsx
@@ -480,6 +480,25 @@ describe('Property filtering', () => {
expect(container?.textContent?.split(',')).toEqual(['a']);
});
+ test('should return precomputed filteringOptions as-is when provided', () => {
+ const precomputed = [
+ { propertyKey: 'id', value: 'custom-1' },
+ { propertyKey: 'id', value: 'custom-2' },
+ ];
+ function App() {
+ const items: Item[] = [{ id: 'one' }, { id: 'two' }];
+ const result = useCollection(items, {
+ propertyFiltering: {
+ filteringProperties: [{ key: 'id', groupValuesLabel: 'Id values', propertyLabel: 'Id' }],
+ filteringOptions: precomputed,
+ },
+ });
+ return <>{result.propertyFilterProps.filteringOptions.map(({ value }) => value).join(',')}>;
+ }
+ const { container } = testRender();
+ expect(container?.textContent?.split(',')).toEqual(['custom-1', 'custom-2']);
+ });
+
test('should not generate filtering options for "falsy" values except boolean false and number zero', () => {
const MixedOptions = () => {
const propertyFiltering = {
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 5294bd0..4c727de 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -47,6 +47,16 @@ export interface UseCollectionOptions {
empty?: React.ReactNode;
noMatch?: React.ReactNode;
filteringProperties: readonly PropertyFilterProperty[];
+ /**
+ * Pre-computed filtering options to use instead of scanning all items.
+ *
+ * When provided, `useCollection` skips its O(items × filteringProperties) scan
+ * and returns this list directly from `propertyFilterProps.filteringOptions`.
+ *
+ * Use this when you already know the full set of valid filter values.
+ * The caller is responsible for keeping this list up-to-date and for deduplication.
+ */
+ filteringOptions?: readonly PropertyFilterOption[];
// custom filtering function
filteringFunction?: (item: T, query: PropertyFilterQuery) => boolean;
defaultQuery?: PropertyFilterQuery;
diff --git a/src/use-collection.ts b/src/use-collection.ts
index 8979ce1..5a038ad 100644
--- a/src/use-collection.ts
+++ b/src/use-collection.ts
@@ -1,9 +1,9 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { useRef } from 'react';
+import { useRef, useMemo } from 'react';
import { processItems, processSelectedItems, itemsAreEqual } from './operations/index.js';
import { UseCollectionOptions, UseCollectionResult, CollectionRef } from './interfaces';
-import { createSyncProps } from './utils.js';
+import { createSyncProps, computeFilteringOptions } from './utils.js';
import { useCollectionState } from './use-collection-state.js';
export function useCollection(allItems: ReadonlyArray, options: UseCollectionOptions): UseCollectionResult {
@@ -61,6 +61,19 @@ export function useCollection(allItems: ReadonlyArray, options: UseCollect
}
}
+ // Memoize `computeFilteringOptions` scan so it only runs when `allItems` or
+ // `filteringProperties` change. When the caller supplies
+ // `options.propertyFiltering.filteringOptions`, the scan is skipped entirely
+ // and the pre-computed list is returned directly.
+ const filteringProperties = options.propertyFiltering?.filteringProperties;
+ const precomputedOptions = options.propertyFiltering?.filteringOptions;
+ const filteringOptions = useMemo(
+ () => computeFilteringOptions(allItems, filteringProperties, precomputedOptions),
+ // filteringProperties and precomputedOptions are typically stable references (defined outside render or
+ // wrapped in useMemo by the caller), so array-identity comparison is correct here.
+ [allItems, filteringProperties, precomputedOptions]
+ );
+
// When normal selection is used, the selectedItems are taken from state.
// When group selection is used, the selectedItems are derived from group selection state.
const extendedState = selectedItems ? { ...state, selectedItems } : state;
@@ -75,6 +88,7 @@ export function useCollection(allItems: ReadonlyArray, options: UseCollect
allItems,
totalItemsCount,
expandableRows,
+ filteringOptions,
}),
};
}
diff --git a/src/utils.ts b/src/utils.ts
index 5aa01ac..a9775c8 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -9,6 +9,7 @@ import {
CollectionRef,
PropertyFilterQuery,
PropertyFilterOption,
+ PropertyFilterProperty,
CollectionActions,
GroupSelectionState,
ExpandableRowsResultBase,
@@ -119,6 +120,44 @@ export function createActions({
};
}
+/**
+ * Scans all items for each filterable property and collects unique values.
+ *
+ * Complexity: O(items × filteringProperties). Callers should memoize the result
+ * so it is only recomputed when `allItems` or `filteringProperties` change.
+ *
+ * When `options.propertyFiltering.filteringOptions` is provided by the caller,
+ * this scan is skipped entirely and the caller-supplied list is returned as-is.
+ */
+export function computeFilteringOptions(
+ allItems: readonly T[],
+ filteringProperties: readonly PropertyFilterProperty[] | undefined,
+ precomputedOptions: readonly PropertyFilterOption[] | undefined
+): PropertyFilterOption[] {
+ if (!filteringProperties) {
+ return [];
+ }
+ if (precomputedOptions !== undefined) {
+ return precomputedOptions as PropertyFilterOption[];
+ }
+ return filteringProperties.reduce((acc, property) => {
+ Object.keys(
+ allItems.reduce<{ [key in string]: boolean }>((acc, item) => {
+ acc['' + fixupFalsyValues(item[property.key as keyof T])] = true;
+ return acc;
+ }, {})
+ ).forEach(value => {
+ if (value !== '') {
+ acc.push({
+ propertyKey: property.key,
+ value,
+ });
+ }
+ });
+ return acc;
+ }, []);
+}
+
export function createSyncProps(
options: UseCollectionOptions,
{
@@ -138,12 +177,14 @@ export function createSyncProps(
allItems,
totalItemsCount,
expandableRows,
+ filteringOptions,
}: {
pagesCount?: number;
actualPageIndex?: number;
allItems: readonly T[];
totalItemsCount: number;
expandableRows?: ExpandableRowsResultBase;
+ filteringOptions: readonly PropertyFilterOption[];
}
): Pick, 'collectionProps' | 'filterProps' | 'paginationProps' | 'propertyFilterProps'> {
let empty: ReactNode | null = options.filtering
@@ -156,24 +197,6 @@ export function createSyncProps(
? options.propertyFiltering.noMatch
: options.propertyFiltering.empty
: empty;
- const filteringOptions = options.propertyFiltering
- ? options.propertyFiltering.filteringProperties.reduce((acc, property) => {
- Object.keys(
- allItems.reduce<{ [key in string]: boolean }>((acc, item) => {
- acc['' + fixupFalsyValues(item[property.key as keyof T])] = true;
- return acc;
- }, {})
- ).forEach(value => {
- if (value !== '') {
- acc.push({
- propertyKey: property.key,
- value,
- });
- }
- });
- return acc;
- }, [])
- : [];
return {
collectionProps: {