Predvoditelev.RU
Заметки

React Select c бесконечной прокруткой

Популярный компонент React Select поддерживает асинхронную загрузку элементов, но при этом загружаются все элементы сразу. В моём случае потребовалось сделать более сложный вариант.

Задача

Нужен компонент Select со следующими возможностями:

  • поиск элементов;
  • динамическая подгрузка элементов по мере перехода пользователя к концу выпадающего списка, так называемая «бесконечная прокрутка»;
  • асинхронная загрузка названия для начального значения.

Решение

Делать будем на базе React Select. Базовые настройки:

const [selected, setSelected] = useState(null);  
const [options, setOptions] = useState([])  
const [isLoading, setIsLoading] = useState(false)

<Select
    // Текущее значение
    value={selected}
    onChange={value => setSelected(value)}
    // Элементы списка
    options={options}
    // Идёт ли загрузка?
    isLoading={isLoading}   
    // Включаем возможность сброса значения
    isClearable={true}   
/>

Далее напишем код, который будет подгружать название для начального значения, если оно задано. Подразумевается, что при GET-запросе по адресу из переменной url с параметром value должна вернуться пустая строка если такой элемент не найден, а если найден, то массив в следующем формате:

[
    value: %ID%
    label: %NAME%
]

Код оборачиваем в useEffect() без зависимостей, чтобы он выполнился один раз. Для загрузки используем библиотеку Axios.

useEffect(() => {
    if (currentValue !== '') {
        const fetchData = async () => {
            setIsLoading(true)
            try {
                const response = await Axios.get(url, {
                    params: {value: currentValue},
                })
                setSelected(response.data)
            } catch (e) {
            } finally {
                setIsLoading(false)
            }
        }
        fetchData()
    }
}, [])

Теперь напишем функцию, которая будет подгружать элементы списка. Здесь мы подразумеваем, что при GET-запросе по адресу из переменной `url` возвращается массив в формате:

[
    'options' => array, // Опции
    'hasNextPage' => boolean, // Есть ли следующая страница?
]

В последующем коде мы используем:

  • isAlreadyOpen — свойство показывает было ли уже открытие списка. Это позволяет нам загружать список не сразу после отрисовки элемента, а отложить это до момента, когда пользователь откроет список.
  • query — запрос, который пользователь ввёл в строку поиска.
  • page — текущая страница.
  • abortController — позволяет отменить незавершённые запросы. Полезно, когда пользователь ввёл новую строку, а мы ещё не получили результаты предыдущего поиска.
const [isAlreadyOpen, setIsAlreadyOpen] = useState(false)  
const [query, setQuery] = useState('')  
const [page, setPage] = useState(1)  

useEffect(() => {
    if (!isAlreadyOpen) {
        return;
    }
    const abortController = new AbortController()
    const fetchData = async () => {
        setIsLoading(true)
        try {
            const response = await Axios.get(url, {
                params: {query, page},
                signal: abortController.signal,
            })
            setOptions(
                page > 1
                    ? options.concat(response.data.options)
                    : response.data.options
            )
            setHasNextPage(response.data.hasNextPage)
            setIsLoading(false)
        } catch (e) {
            if (!Axios.isCancel(e)) {
                setIsLoading(false)
            }
        }
    }
    fetchData()
    return () => abortController.abort()
}, [query, page, isAlreadyOpen])

Осталось немного донастроить элемент Select:

<Select
    hasNextPage={hasNextPage}  

    // Запоминаем, что список уже открыли
    onMenuOpen={() => {  
        setIsAlreadyOpen(true)  
    }}  

    // При прокрутке списка до конца — увеличить страницу на единицу
    onMenuScrollToBottom={() => {  
        if (hasNextPage && !isLoading) {  
            setPage(page + 1)  
        }  
    }}
    
    // ...

Удобно, что React Select имеет свойство onMenuScrollToBottom, не нужно использовать Intersection Observer, который обычно применяется для реализации бесконечной прокрутки.

И последнее — настройка поиска:

<Select
    // При изменении запроса — сбрасываем опции и запускаем загрузку
    // элементов с первой страницы
    onInputChange={  
        debounce(  
            (value) => {  
                if (value !== query) {  
                    setOptions([])  
                    setQuery(value)  
                    setPage(1)  
                    setHasNextPage(true)  
                }  
            },  
            500  
        )  
    }  

    // Отключаем фильтрацию по умолчанию
    filterOption={false}

    // ..

Здесь используется функция debounce из библиотеки Lodash, которая позволяет задать задержку перед выполнением функции, то есть мы запустим поиск через 500 мc после последнего изменения запроса.

Итоговый результат можно посмотреть на GitHub:

При написании статьи использовалось следующие библиотеки:

  • React 18.2
  • React Select 5.8
  • Axios 1.6.8
  • Lodash 4.17.21
@sergei_predvoditelev — Авторский канал в Telegram: заметки о веб-разработке, PHP, открытом ПО, развитии и немного о жизни.