shadcn-ui / ui

Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.
https://ui.shadcn.com
MIT License
58.6k stars 3.2k forks source link

[bug]: Very slow selection with a large amount of elements #3590

Open mr-scrpt opened 2 weeks ago

mr-scrpt commented 2 weeks ago

Describe the bug

I use select to output a large number of items, there can be around 1 thousand and even sometimes more than that When I press select to get a dropdown list, on my device it takes about 3-4 seconds before the list of options appears on the screen. With native select everything works instantly

Affected component/components

Select

How to reproduce

Here is the code for my component

 <FormField
      control={control}
      name="postOffice"
      render={({ field }) => (
        <FormItem>
          <FormLabel>Post office</FormLabel>
          <Select onValueChange={field.onChange} defaultValue={field.value}>
            <FormControl>
              <SelectTrigger>
                <SelectValue placeholder="Select a verified email to display" />
              </SelectTrigger>
            </FormControl>
            <SelectContent>
              {postOfficeListToSelect &&
                postOfficeListToSelect.map((postOffice) => (
                  <SelectItem value={postOffice.value} key={postOffice.value}>
                    {postOffice.label}
                  </SelectItem>
                ))}
            </SelectContent>
          </Select>
        </FormItem>
      )}
    />

Here is what the postOfficeListToSelect array looks like

const postOfficeflistToSelect = [
  {
    value: "7b422fba-e1b8-11e3-8c4a-0050568002cf",
    type: "Branch",
    label: "Post office number 1",
  },
  {
    value: "5db422f-e1b8-11e3-8c4a-0050568002cf",
    type: "Branch",
    label: "Post office number 2",
  },
  ...,
  ...,
  ...,
  {
    value: "158db422f-e1b8-11e3-8c4a-0050568002cf",
    type: "Branch",
    label: "Post office number 1000",
  },
];

Codesandbox/StackBlitz link

No response

Logs

No response

System Info

MacBook Pro M1
Chrome, Ark

Before submitting

OmarAljoundi commented 2 weeks ago

The slownest is coming from rendering to much HTML that any browser can handle fast, when rendering a huge list you might want to consider using some Virtualization library, the way these library works is by rendering only the needed elements within the view it self and as you scrolling it deletes the items the isn't in the view and add new HTML elements which is within the current view.

I made an example where I wanted to select an item in ### 1000 elements list:

Without Virtualization the browser had to load all the HTML content:

https://github.com/shadcn-ui/ui/assets/56927215/25258200-9fa5-4ba1-8b88-2a8cf3d26999

With Virtualization the browser had to load only the screen view HTML content:

https://github.com/shadcn-ui/ui/assets/56927215/39955ef5-397c-4c96-b0b6-dd539487d3f0

Here is a code example: First install react-window: npm i react-window and if you are using typescript you have to also install npm i --save-dev @types/react-window as well

Then in your Select component you need to hide scroll icon button:

const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> & {
    showScrollIcon?: boolean
  }
>(
  (
    {
      className,
      children,
      position = 'popper',
      showScrollIcon = false,
      ...props
    },
    ref
  ) => (
    <SelectPrimitive.Portal>
      <SelectPrimitive.Content
        ref={ref}
        className={cn(
          'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
          position === 'popper' &&
            'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
          className
        )}
        position={position}
        {...props}
      >
        {showScrollIcon && <SelectScrollUpButton />}
        <SelectPrimitive.Viewport
          className={cn(
            'p-1',
            position === 'popper' &&
              'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
          )}
        >
          {children}
        </SelectPrimitive.Viewport>
        {showScrollIcon && <SelectScrollDownButton />}
      </SelectPrimitive.Content>
    </SelectPrimitive.Portal>
  )
)

Then in the SelectContent you need to wrap the items within "FixedSizeList" from "react-window":

 <FixedSizeList
              width={'100%'}
              height={350}
              itemCount={data.length}
              itemSize={40}
            >
              {({ index, style, isScrolling }) => (
                <SelectItem
                  value={data[index]}
                  key={data[index]}
                  style={{
                    ...style,
                  }}
                >
                  {data[index]}
                </SelectItem>
              )}
            </FixedSizeList>

I've used random data generation to genrate a 1000 items for testing the code: const data = Array.from({ length: 1000 }, (_, index) => `Item ${index}`)

Full Code Example:

'use client'
import { useState } from 'react'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select'
import { FixedSizeList } from 'react-window'

const data = Array.from({ length: 1000 }, (_, index) => `Item ${index}`)

export default function MyList() {
  const [value, setValue] = useState('')

  const onChange = (v: string) => {
    setValue(v)
  }

  return (
    <div className="p-16 space-y-8">
      <div>
        <h1 className="text-green-700">With Virtualization</h1>
        <Select onValueChange={onChange} defaultValue={value} open={true}>
          <SelectTrigger>
            <SelectValue placeholder="Select a verified email to display" />
          </SelectTrigger>
          <SelectContent>
            <FixedSizeList
              width={'100%'}
              height={350}
              itemCount={data.length}
              itemSize={40}
            >
              {({ index, style, isScrolling }) => (
                <SelectItem
                  value={data[index]}
                  key={data[index]}
                  style={{
                    ...style,
                  }}
                >
                  {data[index]}
                </SelectItem>
              )}
            </FixedSizeList>
          </SelectContent>
        </Select>
      </div>

      <div>
        <h1 className="text-red-500">Without Virtualization</h1>
        <Select onValueChange={onChange} defaultValue={value}>
          <SelectTrigger>
            <SelectValue placeholder="Select a verified email to display" />
          </SelectTrigger>
          <SelectContent showScrollIcon={false}>
            {data.map((i) => (
              <SelectItem value={i} key={i}>
                {i}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      </div>
    </div>
  )
}
mr-scrpt commented 1 week ago

@OmarAljoundi Thank you, its works for me!!