Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/perf-filtered-action-list-deferred-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@primer/react': patch
---

perf(FilteredActionList): Use useDeferredValue to prevent input lag with large lists

FilteredActionList now uses React's useDeferredValue hook to defer expensive list rendering operations while keeping the text input immediately responsive. This prevents input lag when filtering large lists in SelectPanel.
22 changes: 14 additions & 8 deletions packages/react/src/FilteredActionList/FilteredActionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {ScrollIntoViewOptions} from '@primer/behaviors'
import {scrollIntoView, FocusKeys} from '@primer/behaviors'
import type {KeyboardEventHandler, JSX} from 'react'
import type React from 'react'
import {forwardRef, useCallback, useEffect, useRef, useState} from 'react'
import {forwardRef, useCallback, useDeferredValue, useEffect, useRef, useState} from 'react'
import type {TextInputProps} from '../TextInput'
import TextInput from '../TextInput'
import {ActionList, type ActionListProps} from '../ActionList'
Expand Down Expand Up @@ -133,6 +133,12 @@ export function FilteredActionList({
...listProps
}: FilteredActionListProps): JSX.Element {
const [filterValue, setInternalFilterValue] = useProvidedStateOrCreate(externalFilterValue, undefined, '')

// Use deferred value for items to avoid blocking user input during expensive list rendering
// The immediate filterValue is used for the text input display (keeping typing responsive)
// The deferred items are used for rendering the list, allowing React to defer expensive rendering
const deferredItems = useDeferredValue(items)

const onInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
Expand Down Expand Up @@ -166,15 +172,15 @@ export function FilteredActionList({
const getItemListForEachGroup = useCallback(
(groupId: string) => {
const itemsInGroup = []
for (const item of items) {
for (const item of deferredItems) {
// Look up the group associated with the current item.
if (item.groupId === groupId) {
itemsInGroup.push(item)
}
}
return itemsInGroup
},
[items],
[deferredItems],
)

const onInputKeyDown = useCallback(
Expand Down Expand Up @@ -202,17 +208,17 @@ export function FilteredActionList({
}

const firstGroup = groupMetadata[firstGroupIndex].groupId
firstItem = items.filter(item => item.groupId === firstGroup)[0]
firstItem = deferredItems.filter(item => item.groupId === firstGroup)[0]
} else {
firstItem = items[0]
firstItem = deferredItems[0]
}
if (firstItem.onAction) {
firstItem.onAction(firstItem, event)
event.preventDefault()
}
}
},
[items, groupMetadata, getItemListForEachGroup],
[deferredItems, groupMetadata, getItemListForEachGroup],
)

const onInputKeyPress: KeyboardEventHandler = useCallback(
Expand Down Expand Up @@ -303,7 +309,7 @@ export function FilteredActionList({
}, [loading, inputRef, usingRovingTabindex])

useAnnouncements(
items,
deferredItems,
usingRovingTabindex ? listRef : {current: listContainerElement},
inputRef,
announcementsEnabled,
Expand Down Expand Up @@ -367,7 +373,7 @@ export function FilteredActionList({
</ActionList.Group>
)
})
: items.map(({key: itemKey, ...item}, index) => {
: deferredItems.map(({key: itemKey, ...item}, index) => {
const key = itemKey ?? item.id?.toString() ?? index.toString()
return (
<MappedActionListItem
Expand Down
Loading