Building An Infinite Select Dropdown Component With React
Published: 2024-02-01 14:00:36

by [Stephen Crown](https://blog.openreplay.com/authors/stephen-crown) 


<blockquote><em>
Implementing select dropdown components that can deal with huge datasets is not trivial, and this article will show you how to go about it, avoiding performance problems and providing a slick user experience.
</em></blockquote>

<div style="background-color:#efefef; border-radius:8px; padding:10px; display:block;">
<hr/>
<h3><em>Session Replay for Developers</em></h3> 
<p><em>Uncover frustrations, understand bugs and fix slowdowns like never before with <strong><a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a></strong> — an open-source session replay suite for developers. It can be <strong>self-hosted</strong> in minutes, giving you complete control over your customer data.</em></p>
<img alt="OpenReplay" style="margin-top:5px; margin-bottom:5px;" width="768" height="400" src="https://raw.githubusercontent.com/openreplay/openreplay/main/static/openreplay-git-hero.svg" class="astro-UXNKDZ4E" loading="lazy" decoding="async">
<p><em>Happy debugging! <a href="https://openreplay.com" target="_blank">Try using OpenReplay today.</a></em><p>
<hr/>
</div>

One common challenge web developers face involves handling large datasets, particularly when implementing components like a select dropdown. Usually, developers display all the data options in the dropdown at once. But when large data is involved, this poses significant drawbacks:

- **Performance Issues**: Loading all data at once can significantly slow down the application, especially if there's a large volume of data. Users might experience delays or crashes due to the overwhelming amount of information being loaded simultaneously.
- **High Resource Consumption**: Fetching and rendering all data simultaneously consumes much bandwidth and system resources. It can lead to increased server load, higher data usage, and reduced battery life on mobile devices.

To address these, we'll display the data in batches. Initially, we will show the first `n` items (say 10). Then, users can trigger an action to view the subsequent ten items and repeat this process. Pagination typically enables users to navigate through data sets using a next button, a previous button, and numbered links. However, this approach isn't practical for a select dropdown because we can’t display the buttons/links inside the dropdown. Instead, we will use the infinite scroll technique, which shares the same premise as classic pagination but eliminates the need for button clicks or numbered links. Instead, as users scroll to the bottom of the displayed options, the next set of options is loaded, continuing until all options are fetched. Users can revisit earlier options to scroll back to the previously loaded data.

We can optimize this component further by including a search field. This combination ensures that users never need to load all options upfront; they can search for what they need regardless of its position in the list, enhancing the component's efficiency. 

We will implement the infinite scrolling using the Intersection Observer API.

## The Intersection Observer API
The [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) detects when an element intersects with the browser's viewport or a parent element's viewport. For a select dropdown component, we can use it to identify when the last option(s) is displayed within the parent container. If this occurs, we can trigger a callback to fetch additional data.

## Creating the Select Dropdown Component
We'll kick off by creating the base select dropdown component. While we'll use TailwindCSS for styling and TypeScript for typing, it's worth noting that neither TailwindCSS nor TypeScript is mandatory for this implementation.

To integrate this component, you can add it to an existing React application or start with a new one. If you're starting fresh, create a new React app using `create-react-app`:

```bash
npx create-react-app my-app --template typescript
cd my-app
```

Once your React app is set up (or if you're using an existing project), proceed by creating a new file `src/Components/Select/Select.tsx` for the select dropdown component and paste the code below:

```javascript
import { FC, useState } from 'react'
import { SelectOptionProps, SelectProps } from 'Types/SelectTypes'

import { GreaterThanIcon } from 'Assets/Svgs'
import { useListenForOutsideClicks } from 'Hooks'

const Select: FC<SelectProps> = ({ options, selected = { label: '', value: '' }, placeholder = 'Select', handleSelect }) => {
  const [isDropdownOpen, setIsDropdownOpen] = useState(false)

  const openDropdown = () => {
    setIsDropdownOpen(true)
  }

  const closeDropdown = () => {
    setIsDropdownOpen(false)
  }

  const labelClassName = () => {
    return `block max-w-full capitalize truncate ${selected?.label ? 'text-black' : 'text-neutral/400'}`
  }

  const optionClassName = (option: SelectOptionProps, index: number, isSelected: boolean) => {
    isSelected ||= selected?.value === option.value

    return `active:bg-background-selected-option relative cursor-default select-none py-2 px-4 ${
      options.length - 1 === index ? 'rounded-b-md' : ''
    } ${isSelected ? 'bg-secondary/blue/50' : ''} hover:bg-secondary/blue/50 mb-1 last-of-type:mb-[0] block text-left w-full`
  }

  const containerClassName = () => `
    ${
      isDropdownOpen ? '!border-grey/900' : ''
    } px-4 py-2 flex justify-between items-center rounded w-full font-normal border border-solid border-neutral/200 bg-transparent leading-[20px] text-xs text-grey/900
    `

  const { elementRef } = useListenForOutsideClicks(closeDropdown)

  const renderOptions = (options: SelectOptionProps[]) => {
    return options?.length > 0 ? (
      options?.map((option, index) => {
        const isSelected = selected?.value === option.value

        return (
          <button
            type='button'
            key={String(option.value) + String(index)}
            className={optionClassName(option, index, selected?.value === option.value)}
            onClick={() => {
              handleSelect(option)
              closeDropdown()
            }}
          >
            <span
              title={option.label}
              className={`${
                isSelected ? 'font-semibold ' : 'font-normal'
              } block truncate text-black text-[0.625rem] cursor-pointer leading-[0.8rem] font-normal`}
            >
              {option.label}
            </span>
          </button>
        )
      })
    ) : (
      <div className='relative cursor-default select-none py-2 pl-3 pr-9'>
        <span className='font-normal block truncate text-sm text-black'>No options here</span>
      </div>
    )
  }

  return (
    <div className='relative grow'>
      <button type='button' onClick={openDropdown} className={containerClassName()}>
        <span title={selected?.label} className={labelClassName()}>
          {selected?.label || placeholder}
        </span>
        <span className='pointer-events-none ml-3 flex items-center'>
          <GreaterThanIcon className='rotate-90 text-[#96989A]' />
        </span>
      </button>

      {isDropdownOpen && (
        <div
          className={
            'absolute z-[500] w-full overflow-auto rounded-b-md bg-shades/white py-[14px] text-base ring-opacity-5 focus:outline-none mt-1 max-h-40 '
          }
          style={{ boxShadow: '0px 4px 10px rgba(0, 0, 0, 0.08)' }}
          ref={elementRef}
        >
          {renderOptions(options)}
        </div>
      )}
    </div>
  )
}

export default Select
```

In the component, we manage the open/close state of the dropdown with the `isDropdownOpen` state, which is a boolean. Alongside this, we create some functions to manage different aspects of the component:

- `openDropdown` sets the `isDropdownOpen` state to `true` and is used to open the dropdown.

- `closeDropdown` sets the `isDropdownOpen` state to `false` and is used to close the dropdown.

- `labelClassName` dynamically generates and returns the CSS class name for the component's label based on whether an item is selected. It helps apply different styles when a selected label or placeholder is displayed.

- `optionClassName` provides dynamic class names for each dropdown option. It takes into account whether the option is selected and the position of the option in the list, which aids in styling each option appropriately.

- `containerClassName` generates the CSS class for the dropdown container, including adjustments based on whether the dropdown is open or closed.

- `renderOptions` handles the rendering of dropdown options. It maps over the options array and returns a button element for each option or a message if there are no options.

Inside this component, we reference TypeScript types `SelectOptionProps` and `SelectProps` to enforce a consistent structure for the dropdown options and the component props, respectively. We also use a custom hook named `useListenForOutsideClicks` to close the dropdown when the user clicks outside it.

We will now define these types in `src/Types/SelectTypes.ts`. Here's the code for these type definitions:

```javascript
export type SelectOptionProps = {
  label: string // displayed label
  value: string // value used in computation
}

export type SelectProps = {
  options: SelectOptionProps[] // an array of the options.
  selected?: SelectOptionProps // the selected option.
  handleSelect: (option: SelectOptionProps) => void // function that is called when an option is selected.
  placeholder?: string
}
```

Then, we define the `useListenForOutsideClicks` hook in     `src/Hooks/useListenForOutsideClicks.ts`:

```javascript
import { useEffect, useRef } from 'react'

/**
 * A custom React hook that listens for clicks outside of a specified element
 * and executes a callback function when detected.
 *
 * @param onOutsideClick The callback function to execute when an outside click is detected
 *
 * @returns A map object containing the following properties:
 *    `elementRef`: A React ref object that should be attached to the element that
 *                  should listen for outside clicks
 *
 */
const useListenForOutsideClicks = (onOutsideClick: () => void) => {
  const elementRef = useRef<any>(null)

  useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      if (elementRef.current && !elementRef.current.contains(event.target)) {
        onOutsideClick?.()
      }
    }
    document.addEventListener('click', handleClickOutside, true)
    return () => {
      document.removeEventListener('click', handleClickOutside, true)
    }
  }, [onOutsideClick])

  return { elementRef }
}

export default useListenForOutsideClicks
```

When you attach this hook to a component, it monitors clicks anywhere in the document. The hook triggers a callback function if a click occurs outside the specified element. This function, passed as `onOutsideClick`, typically contains instructions to alter the parent component's state, like closing a dropdown menu.

The core of this hook lies in its use of a React ref (`elementRef`) created with `useRef`. `elementRef` is attached to the element we want to monitor. The hook then sets up an event listener using `useEffect` that listens for click events on the document. The listener checks whether the click occurred outside the `elementRef`. If it did, the onOutsideClick function is executed.

Finally, we use the select dropdown component in our main application file, `src/App.tsx`. We can import and use the component as part of our application's UI. Here's an example of how to incorporate the `<Select />` component in App.tsx:

```javascript
import { Select } from 'Components'
import { SelectOptionProps } from 'Types/SelectTypes'
import { useState } from 'react'

function App() {
  const [selectedOption, setSelectedOption] = useState<SelectOptionProps>({ label: '', value: '' })

  const handleSelect = (option: SelectOptionProps) => {
    setSelectedOption(option)
  }

  const options: SelectOptionProps[] = [
    { label: 'Option 1', value: 'option-1' },
    { label: 'Option 2', value: 'option-2' },
    { label: 'Option 3', value: 'option-3' },
  ]

  return (
    <div className='p-20'>
      <div className='block w-52'>
        <span className='block mb-2 text-sm'>Select product</span>
        <Select options={options} selected={selectedOption} placeholder='Select product' handleSelect={handleSelect} />
      </div>
    </div>
  )
}

export default App
```

In the `App` component, we demonstrate using the `Select` component. We define a `selectedOption` state to track the currently selected item. This state is updated through the `handleSelect` function, which we pass to the `Select` component, alongside `options`, a placeholder and `selectedOption`.

When we start our app, we should see a working Select dropdown component in our browser with three options.

![Screenshot 2023-12-26 at 12.54.19 PM](https://blog.openreplay.com/images/infinite-select-dropdown-select-component-with-react/images/image1.png)


## Populating the Dropdown Options
Next, we will populate our dropdown options with data from an API. We will use the https://dummyjson.com/ API to fetch a list of products, map through the products, and display the product titles while using the product ID as values.

We will start by fetching the products in `src/App.tsx` and passing it to the `Select` component, replacing the earlier dummy options. We'll also introduce a boolean value,     `isFetchingProducts`, indicating the data fetching status which we will pass to the component and will be used to display a loader to the user during data retrieval.

```javascript 
import { useEffect, useState } from 'react'

import { Select } from 'Components'
import { SelectOptionProps } from 'Types/SelectTypes'

function App() {
  const [selectedOption, setSelectedOption] = useState<SelectOptionProps>({ label: '', value: '' })
  const [productOptions, setProductOptions] = useState<SelectOptionProps[]>([])
  const [isFetchingProducts, setIsFetchingProducts] = useState(true)

  const handleSelect = (option: SelectOptionProps) => {
    setSelectedOption(option)
  }

  const transformProductToSelectOptions = (products: { title: string; id: number }[]) => {
    if (!products) return []

    return products?.map((product) => {
      return {
        label: product?.title,
        value: product?.id,
      }
    })
  }

  const fetchAndSetProducts = async () => {
    try {
      const response = await fetch('https://dummyjson.com/products')
      const data = await response.json()
      setProductOptions(transformProductToSelectOptions(data?.products))
    } catch (error) {
      alert('Something went wrong')
    } finally {
      setIsFetchingProducts(false)
    }
  }

  useEffect(() => {
    fetchAndSetProducts()
  }, [])

  return (
    <div className='p-20'>
      <div className='block w-52'>
        <span className='block mb-2 text-sm'>Select product</span>
        <Select
          options={productOptions}
          selected={selectedOption}
          placeholder='Select product'
          handleSelect={handleSelect}
          isFetchingOptions={isFetchingProducts}
        />
      </div>
    </div>
  )
}

export default App
```

Upon loading, the component executes the `fetchAndSetProducts` function, which retrieves product data from the API. Once the fetch is successful, the data transforms by the `transformProductToSelectOptions` function. This function converts the product data into an array of objects, each with a `label` and a `value`, aligning with the expected format for the `Select` component. This transformed array is then stored in the `productOptions` state. The `Select` component receives these product options as its `options` prop. Additionally, the `isFetchingProducts` state, indicating the loading status, is passed to the `Select` component to manage the display of a loader during data fetching. 

Next we will update the `Select` component (`src/Components/Select/Select.tsx`) to display a loader when `isFetchingProducts` is true.

```javascript 
// src/Components/Select/Select.tsx

import { FC, useState } from 'react'
import { SelectOptionProps, SelectProps } from 'Types/SelectTypes'

import { GreaterThanIcon } from 'Assets/Svgs'
import { Loader } from 'Components'
import { useListenForOutsideClicks } from 'Hooks'

const Select: FC<SelectProps> = ({ options, isFetchingOptions, selected = { label: '', value: '' }, placeholder = 'Select', handleSelect }) => {
  const [isDropdownOpen, setIsDropdownOpen] = useState(false)

  const openDropdown = () => {
    setIsDropdownOpen(true)
  }

  const closeDropdown = () => {
    setIsDropdownOpen(false)
  }

  const labelClassName = () => {
    return `block max-w-full capitalize truncate ${selected?.label ? 'text-black' : 'text-neutral/400'}`
  }

  const optionClassName = (option: SelectOptionProps, index: number, isSelected: boolean) => {
    isSelected ||= selected?.value === option.value

    return `active:bg-background-selected-option relative cursor-default select-none py-2 px-4 ${
      options.length - 1 === index ? 'rounded-b-md' : ''
    } ${isSelected ? 'bg-secondary/blue/50' : ''} hover:bg-secondary/blue/50 mb-1 last-of-type:mb-[0] block text-left w-full`
  }

  const containerClassName = () => `
    ${
      isDropdownOpen ? '!border-grey/900' : ''
    } px-4 py-2 flex justify-between items-center rounded w-full font-normal border border-solid border-neutral/200 bg-transparent leading-[20px] text-xs text-grey/900
    `

  const { elementRef } = useListenForOutsideClicks(closeDropdown)

  const renderNoOptions = () => {
    if (isFetchingOptions) return <Loader />

    return (
      <div className='relative cursor-default select-none py-2 pl-3 pr-9'>
        <span className='font-normal block truncate text-sm text-black'>No options here</span>
      </div>
    )
  }

  const renderOptions = (options: SelectOptionProps[]) => {
    return options?.length > 0
      ? options?.map((option, index) => {
          const isSelected = selected?.value === option.value

          return (
            <button
              type='button'
              key={String(option.value) + String(index)}
              className={optionClassName(option, index, selected?.value === option.value)}
              onClick={() => {
                handleSelect(option)
                closeDropdown()
              }}
            >
              <span
                title={option.label}
                className={`${
                  isSelected ? 'font-semibold ' : 'font-normal'
                } block truncate text-black text-[0.625rem] cursor-pointer leading-[0.8rem] font-normal`}
              >
                {option.label}
              </span>
            </button>
          )
        })
      : renderNoOptions()
  }

  return (
    <div className='relative grow'>
      <button type='button' onClick={openDropdown} className={containerClassName()}>
        <span title={selected?.label} className={labelClassName()}>
          {selected?.label || placeholder}
        </span>
        <span className='pointer-events-none ml-3 flex items-center'>
          <GreaterThanIcon className='rotate-90 text-[#96989A]' />
        </span>
      </button>

      {isDropdownOpen && (
        <div
          className={
            'absolute z-[500] w-full overflow-auto rounded-b-md bg-shades/white py-[14px] text-base ring-opacity-5 focus:outline-none mt-1 max-h-40 '
          }
          style={{ boxShadow: '0px 4px 10px rgba(0, 0, 0, 0.08)' }}
          ref={elementRef}
        >
          {renderOptions(options)}
        </div>
      )}
    </div>
  )
}

export default Select
```

In the `renderOptions` function, when the `options` array is empty, it calls `renderNoOptions`. This function conditionally renders a ``<Loader />`` component if `isFetchingOptions` is `true`, indicating data is being fetched. Otherwise, it displays a message indicating that there are no options available.

Next, we will update the `SelectProps` type in `src/Types/SelectTypes.ts` to include `isFetchingOptions.`

```javascript 
export type SelectProps = {
  options: SelectOptionProps[] // an array of the options.
  selected?: SelectOptionProps // the selected option.
  handleSelect: (option: SelectOptionProps) => void // function that is called when an option is selected.
  placeholder?: string
  isFetchingOptions?: boolean
}
```

Finally, we will create our `<Loader />` component in `src/Components/Loader/Loader.tsx`

```javascript 
const Loader = () => {
  return (
    <div className='flex justify-center items-center '>
      <div className='animate-spin rounded-full h-4 w-4 border-t-2 border-black '></div>
    </div>
  )
}

export default Loader
```

If all goes well, you should see the product titles from the API in the dropdown menu of the `<Select />` component.


<CTA_Middle_Basics />



## Adding Infinite Scroll to the Select Component

As mentioned, we will use the Intersection Observer API to enable infinite scrolling. First, we’ll add parameters to the API endpoint to support pagination. API endpoints support pagination in different ways, and this is often outlined in their documentation. The API we use in this guide requires us to pass the `limit` and `skip` parameters. `limit` dictates the number of items to return, while `skip` specifies the number of items to bypass before fetching. For instance, with a `limit` of 5 and a `skip` of 2, it skips the initial two items (index 0 and 1) and fetches items from index 2 to 6 (totaling five).

To implement infinite scrolling in our component, we will begin by creating a `useIntersectionObserver` hook (`src/Hooks/useIntersectionObserver.ts`) dedicated to managing intersection checks. This hook will update the `page` state once the referenced elements enter the viewport. It will return some values and methods its calling components can use to implement infinite scrolling.

```javascript 
import { useCallback, useRef, useState } from 'react'

const useIntersectionObserver = (isDataLoading: boolean) => {
  const [page, setPage] = useState(1)
  const [hasMore, setHasMore] = useState(true)

  const observer = useRef<IntersectionObserver | null>(null)

  const lastEntryRef = useCallback(
    (node: Element | null) => {
      if (isDataLoading) return

      if (observer.current) observer.current.disconnect()

      observer.current = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting && hasMore) {
          // update page.
          setPage((prev) => prev + 1)
        }
      })

      if (node) observer.current.observe(node)
    },
    [isDataLoading, hasMore]
  )

  return { lastEntryRef, setHasMore, setPage, page }
}

export default useIntersectionObserver
```

This hook uses a combination of React's `useState`, `useRef`, and `useCallback` hooks to manage pagination and observe when a user scrolls to the end of a list or page. The hook initializes state variables for tracking the current page (`page`) and whether there are more items to load (`hasMore`). An `IntersectionObserver` detects when the last item in the current list comes into view. Suppose data is not loading (`isDataLoading` is false) and more items are available (`hasMore` is true). In that case, the hook increases the page number, indicating that the next set of items should be loaded. This logic, wrapped in the `useCallback` hook, ensures efficient, automatic loading of new content as the user scrolls.

The hook returns elements to the consuming component. It returns the `lastEntryRef` callback ref, which is used to assign the element to be observed for intersection changes. This is typically the last item in the list, enabling the detection of when the user has scrolled to the bottom. The hook also provides `setHasMore` and `setPage` functions, allowing the component to update whether more items are available to load and manually set the page number if needed. Finally, it returns the current page state (`page`), giving the component access to the current pagination status. 

Next, we will make use of this hook in `src/App.tsx`:

```javascript
import { useEffect, useState } from 'react'

import { Select } from 'Components'
import { SelectOptionProps } from 'Types/SelectTypes'
import { useIntersectionObserver } from 'Hooks'

const LIMIT = 5

function App() {
  const [selectedOption, setSelectedOption] = useState<SelectOptionProps>({ label: '', value: '' })
  const [productOptions, setProductOptions] = useState<SelectOptionProps[]>([])
  const [isFetchingProducts, setIsFetchingProducts] = useState(true)
  const [totalItems, setTotalItems] = useState(0)

  const handleSelect = (option: SelectOptionProps) => {
    setSelectedOption(option)
  }

  const transformProductToSelectOptions = (products: { title: string; id: number }[]) => {
    if (!products) return []

    return products?.map((product) => {
      return {
        label: product?.title,
        value: product?.id,
      }
    })
  }

  const { lastEntryRef, setHasMore, setPage, page } = useIntersectionObserver(isFetchingProducts)

  useEffect(() => {
    if (totalItems === 0) return
    if (!isFetchingProducts) {
      setHasMore(productOptions?.length < totalItems)
    }
  }, [productOptions, totalItems])

  const getSkipValue = () => {
    return (page - 1) * LIMIT
  }

  const fetchAndSetProducts = async () => {
    try {
      setIsFetchingProducts(true)
      const response = await fetch(`https://dummyjson.com/products?limit=${LIMIT}&skip=${getSkipValue()}`)
      const data = await response.json()

      if (page === 1) setProductOptions([])

      setProductOptions((prev) => [...prev, ...transformProductToSelectOptions(data?.products)])
      setTotalItems(data?.total)
    } catch (error) {
      alert('Something went wrong')
      console.log({ error })
    } finally {
      setIsFetchingProducts(false)
    }
  }

  useEffect(() => {
    fetchAndSetProducts()
  }, [page])

  return (
    <div className='p-20'>
      <div className='block w-52'>
        <span className='block mb-2 text-sm'>Select product</span>
        <Select
          options={productOptions}
          selected={selectedOption}
          placeholder='Select product'
          handleSelect={handleSelect}
          isFetchingOptions={isFetchingProducts}
          lastOptionRef={lastEntryRef}
        />
      </div>
    </div>
  )
}

export default App
```

We append `limit` and `skip` params to the API endpoint. Then we use the `useIntersectionObserver` hook passing the `isFetchingProducts` state to it. We make use of the functions and variables returned by the hook. We pass `lastEntryRef` to the `lastOptionRef` of the `<Select />` component which we will handle later. Then we use `setHasMore` to set `hasMore` to `true` if length of `productOptions` is less than `totalItems`, and `false` and `isFetchingProducts` is `false`. We will use `setPage` in the next section.

Finally, let's update the `<Select />` component to pass `lastOptionRef` to the last item in the dropdown. The only thing to change is in the `renderOptions` function:

```javascript 
const renderOptions = (options: SelectOptionProps[]) => {
    return options?.length > 0
      ? options?.map((option, index) => {
          const isSelected = selected?.value === option.value

          return (
            <button
              type='button'
              key={String(option.value) + String(index)}
              className={optionClassName(option, index, selected?.value === option.value)}
              onClick={() => {
                handleSelect(option)
                closeDropdown()
              }}
              ref={options?.length - 1 === index ? lastOptionRef : null}
            >
              <span
                title={option.label}
                className={`${
                  isSelected ? 'font-semibold ' : 'font-normal'
                } block truncate text-black text-[0.625rem] cursor-pointer leading-[0.8rem] font-normal`}
              >
                {option.label}
              </span>
            </button>
          )
        })
      : renderNoOptions()
  }
```

Here, we pass `lastOptionRef` as `ref` of the last item:

```javascript 
ref={options?.length - 1 === index ? lastOptionRef : null}
```

We also need to update the `SelectProps` type (`src/Types/SelectTypes.ts`) to accommodate this new prop `lastOptionRef`:


```javascript 
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from 'react'

import { Select } from 'Components'
import { SelectOptionProps } from 'Types/SelectTypes'
import { useIntersectionObserver } from 'Hooks'

const LIMIT = 5

function App() {
  const [selectedOption, setSelectedOption] = useState<SelectOptionProps>({ label: '', value: '' })
  const [productOptions, setProductOptions] = useState<SelectOptionProps[]>([])
  const [isFetchingProducts, setIsFetchingProducts] = useState(true)
  const [totalItems, setTotalItems] = useState(0)
  const [searchInput, setSearchInput] = useState('')
  const [debouncedSearchInput, setDebouncedSearchInput] = useState('')

  const handleSelect = (option: SelectOptionProps) => {
    setSearchInput(option?.label)
    setSelectedOption(option)
  }

  const transformProductToSelectOptions = (products: { title: string; id: number }[]) => {
    if (!products) return []

    return products?.map((product) => {
      return {
        label: `${product?.title}`,
        value: product?.id,
      }
    })
  }

  const { lastEntryRef, setHasMore, setPage, page } = useIntersectionObserver(isFetchingProducts)

  useEffect(() => {
    if (totalItems === 0) return
    if (!isFetchingProducts) {
      setHasMore(productOptions?.length < totalItems)
    }
  }, [productOptions, totalItems])

  const getSkipValue = () => {
    return (page - 1) * LIMIT
  }

  const getApiUrl = () => {
    if (debouncedSearchInput) {
      return `https://dummyjson.com/products/search?q=${debouncedSearchInput}&limit=${LIMIT}&skip=${getSkipValue()}`
    } else {
      return `https://dummyjson.com/products?limit=${LIMIT}&skip=${getSkipValue()}`
    }
  }

  useEffect(() => {
    const timeoutId = setTimeout(() => {
      setProductOptions([])
      setPage(1)
      setDebouncedSearchInput(searchInput)
    }, 500) // delay fetching by 500ms

    return () => {
      clearTimeout(timeoutId)
    }
  }, [searchInput])

  const fetchAndSetProducts = async () => {
    try {
      setIsFetchingProducts(true)
      const response = await fetch(getApiUrl())
      const data = await response.json()

      if (page === 1) setProductOptions([])

      setProductOptions((prev) => [...prev, ...transformProductToSelectOptions(data?.products)])
      setTotalItems(data?.total)
    } catch (error) {
      alert('Something went wrong')
      console.log({ error })
    } finally {
      setIsFetchingProducts(false)
    }
  }

  useEffect(() => {
    fetchAndSetProducts()
  }, [page, debouncedSearchInput])

  return (
    <div className='p-20'>
      <div className='block w-52'>
        <span className='block mb-2 text-sm'>Select product</span>
        <Select
          options={productOptions}
          selected={selectedOption}
          placeholder='Select product'
          handleSelect={handleSelect}
          isFetchingOptions={isFetchingProducts}
          lastOptionRef={lastEntryRef}
          isSearchable={true}
          setSearchInput={setSearchInput}
          searchInput={searchInput}
        />
      </div>
    </div>
  )
}

export default App
```

We create a `searchInput` and `debouncedSearchInput` state to handle user input for search and trigger the API call after a brief delay, set at 500 milliseconds. This debouncing technique ensures that the API call is made efficiently only after the user has stopped typing for the specified duration rather than on every keystroke. To integrate this new search capability, we update the `fetchAndSetProducts` function. Now, it constructs the API URL dynamically, considering the current value of `debouncedSearchInput`. This modification allows the function to fetch products based on the user's search criteria. Then we pass the necessary search-related props (`isSearchable`, `setSearchInput`, and `searchInput`) to the Select component.


Next, we will teach the `<Select />` component (`src/Components/Select/Select.tsx`) how to handle these new props. It will display an input field if `isSearchable` is `true`, and update the search state when the field changes using `setSearchInput`:


```javascript 
import { FC, useState } from 'react'
import { SelectOptionProps, SelectProps } from 'Types/SelectTypes'

import { GreaterThanIcon } from 'Assets/Svgs'
import { Loader } from 'Components'
import { useListenForOutsideClicks } from 'Hooks'

const Select: FC<SelectProps> = ({
  options,
  isFetchingOptions,
  lastOptionRef,
  isSearchable,
  searchInput,
  selected = { label: '', value: '' },
  placeholder = 'Select',
  handleSelect,
  setSearchInput,
}) => {
  const [isDropdownOpen, setIsDropdownOpen] = useState(false)

  const openDropdown = () => {
    setIsDropdownOpen(true)
  }

  const closeDropdown = () => {
    setIsDropdownOpen(false)
  }

  const labelClassName = () => {
    return `block max-w-full capitalize truncate ${selected?.label ? 'text-text-tertiary' : 'text-neutral/400'}`
  }

  const optionClassName = (option: SelectOptionProps, index: number, isSelected: boolean) => {
    isSelected ||= selected?.value === option.value

    return `active:bg-background-selected-option relative cursor-default select-none py-2 px-4 ${
      options.length - 1 === index ? 'rounded-b-md' : ''
    } ${isSelected ? 'bg-secondary/blue/50' : ''} hover:bg-secondary/blue/50 mb-1 last-of-type:mb-[0] block text-left w-full`
  }

  const containerClassName = () => `
    ${
      isDropdownOpen ? '!border-grey/900' : ''
    } px-4 py-2 flex justify-between items-center rounded w-full font-normal border border-solid border-neutral/200 bg-transparent leading-[20px] text-xs text-grey/900
    `

  const { elementRef } = useListenForOutsideClicks(closeDropdown)

  const renderNoOptions = () => {
    if (isFetchingOptions) return <Loader />

    return (
      <div className='relative cursor-default select-none py-2 pl-3 pr-9'>
        <span className='font-normal block truncate text-sm text-text-tertiary'>No options here</span>
      </div>
    )
  }

  const renderOptions = (options: SelectOptionProps[]) => {
    return options?.length > 0
      ? options?.map((option, index) => {
          const isSelected = selected?.value === option.value

          return (
            <button
              type='button'
              key={String(option.value) + String(index)}
              className={optionClassName(option, index, selected?.value === option.value)}
              onClick={() => {
                handleSelect(option)
                closeDropdown()
              }}
              ref={options?.length - 1 === index ? lastOptionRef : null}
            >
              <span
                title={option.label}
                className={`${
                  isSelected ? 'font-semibold ' : 'font-normal'
                } block truncate text-text-tertiary text-[0.625rem] cursor-pointer leading-[0.8rem] text-shades/black font-normal`}
              >
                {option.label}
              </span>
            </button>
          )
        })
      : renderNoOptions()
  }

  return (
    <div className='relative grow'>
      <button onClick={openDropdown} className={containerClassName()}>
        {isSearchable ? (
          <input
            type='text'
            className='block text-text-tertiary w-full outline-none'
            onChange={(ev) => {
              setSearchInput?.(ev.target.value)
            }}
            placeholder={placeholder}
            value={searchInput}
          />
        ) : (
          <span title={selected?.label} className={labelClassName()}>
            {selected?.label || placeholder}
          </span>
        )}
        <span className='pointer-events-none ml-3 flex items-center'>
          <GreaterThanIcon className='rotate-90 text-[#96989A]' />
        </span>
      </button>

      {isDropdownOpen && (
        <div
          className={
            'absolute z-[500] w-full overflow-auto rounded-b-md bg-shades/white py-[14px] text-base ring-opacity-5 focus:outline-none mt-1 max-h-40 '
          }
          style={{ boxShadow: '0px 4px 10px rgba(0, 0, 0, 0.08)' }}
          ref={elementRef}
        >
          {renderOptions(options)}

          {isFetchingOptions && options?.length > 0 && <Loader />}
        </div>
      )}
    </div>
  )
}

export default Select
```

Again, we add these new fields to the `SelectProps` type (`src/Types/SelectTypes.ts`):
```javascript 
export type SelectProps = {
  options: SelectOptionProps[] // an array of the options.
  selected?: SelectOptionProps // the selected option.
  handleSelect: (option: SelectOptionProps) => void // function that is called when an option is selected.
  placeholder?: string
  isFetchingOptions?: boolean
  isSearchable?: boolean
  searchInput?: string
  lastOptionRef?: (node: Element | null) => void
  setSearchInput?: React.Dispatch<React.SetStateAction<string>>
}
```


## Conclusion
We explored building an optimized select dropdown using the Intersection Observer API for infinite scrolling. It's worth noting that this technique extends beyond select dropdowns. Components such as tables, comment lists, and others that display a list of similar data items can also benefit from infinite scrolling using a similar approach.