reactjstypescriptvirtualizationreact-selectreact-virtualized

How to Virtualize the options for React-Select?


I've been trying to implement virtualization to a React-Select component although without success. Every single virtualization package that I have tried using has had some sort of breaking fault that I haven't been able to solve, to be precise:

It could be that I'm messing something up due to my lack of experience, but even when directly copy-pasting other people's solutions, I end up with a buggy mess. I believe this has to do with most of the virtualization packages above not being fully adapted for select v5 or TypeScript or React 18 etc.

Any help would be more than appreciated!! Thank you in advance :)


Solution

  • Answer related to this point: react-virtualized: For some reason, still slightly choppy, also breaking graphical bugs (Like any text that would be in a second row shows up in the next Option)


    To prevent second row shows up in the next option you need a CellMeasurerCache

    When list size (width) changes it will automatically recalculate height of each row in visible frame.

    Of course it not a perfect solution but it hits your requirements

    import React, { useMemo } from 'react'
    import AsyncSelect from 'react-select/async'
    import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized'
    import type { MenuListProps, GroupBase } from 'react-select'
    import type { ListRowProps } from 'react-virtualized'
    
    type ListOption = {
      label: string
      value: string
    }
    
    const VirtualizedList = ({ children }: MenuListProps<ListOption, false, GroupBase<ListOption>>) => {
      const rows = children
    
      const cellCache = useMemo(() => {
        return new CellMeasurerCache({
          fixedWidth: true,
          defaultHeight: 30
        })
      }, [])
    
      if (!Array.isArray(rows)) {
        // For children like: "Loading" or "No Options" provided by 'react-select'
        return <>{children}</>
      }
    
      const rowRenderer = ({ key, parent, index, style }: ListRowProps) => (
        <CellMeasurer cache={cellCache} key={key} columnIndex={0} rowIndex={index} parent={parent}>
          <div key={key} style={style}>
            {rows[index]}
          </div>
        </CellMeasurer>
      )
    
      return (
        <div style={{ height: '300px' }}>
          <AutoSizer>
            {({ width, height }) => (
              <List
                width={width}
                height={height}
                deferredMeasurementCache={cellCache}
                rowHeight={cellCache.rowHeight}
                rowCount={rows.length}
                rowRenderer={rowRenderer}
              />
            )}
          </AutoSizer>
        </div>
      )
    }
    
    export const Example = () => {
      return (
        <div>
          <AsyncSelect
            cacheOptions
            components={{ MenuList: VirtualizedList }}
            defaultOptions={'YOUR DEFAULT DATA'}
            loadOptions={'YOUR DATA FROM SOMEWHERE'}
            styles={{
              menu: ({ position, fontWeight, ...provided }) => ({
                ...provided,
                position: 'static'
              })
            }}
          />
        </div>
      )
    }
    
    

    Example with cache recalculation triggered by changing of row width