Асинхронная загрузка данных через fetch API в Choices.js позволяет превращать обычные селекты в динамические компоненты, работающие с удалёнными источниками данных. Такой подход используется при больших справочниках, поиске по базе пользователей, тегах, товарах и любых сценариях, где список опций не может быть заранее загружен целиком.
Основная идея интеграции заключается в том, что Choices.js не хранит полный набор данных локально, а получает его порциями по мере ввода пользователя. Для этого используется событие ввода, обработка запроса и вызов fetch к серверному API.
Связка Choices.js и fetch API строится вокруг нескольких ключевых этапов:
Choices.js ожидает данные в формате:
{
value: "id",
label: "Название"
}
или расширенный вариант:
{
value: "id",
label: "Название",
selected: false,
disabled: false
}
Для работы с fetch обычно отключается первоначальная загрузка опций и включается режим поиска:
const element = document.querySelector('#users');
const choices = new Choices(element, {
searchEnabled: true,
shouldSort: false,
placeholderValue: 'Начните ввод...',
loadingText: 'Загрузка...',
noResultsText: 'Ничего не найдено',
});
На этом этапе библиотека готова принимать динамические данные, но ещё не подключена к серверу.
Choices.js предоставляет события, позволяющие реагировать на ввод
пользователя. Наиболее часто используется событие
search.
element.addEventListener('search', async (event) => {
const query = event.detail.value;
const response = await fetch(`/api/users?q=${encodeURIComponent(query)}`);
const data = await response.json();
const formatted = data.map(user => ({
value: user.id,
label: user.name
}));
choices.setChoices(formatted, 'value', 'label', true);
});
Метод setChoices полностью заменяет текущие элементы
списка.
Для корректной работы fetch API важно, чтобы сервер возвращал предсказуемую структуру данных.
Пример ответа:
[
{ "id": 1, "name": "Алексей Иванов" },
{ "id": 2, "name": "Мария Петрова" }
]
Расширенный вариант для поиска:
{
"items": [
{ "id": 1, "name": "Алексей Иванов" },
{ "id": 2, "name": "Мария Петрова" }
],
"total": 120
}
Во втором случае требуется дополнительная обработка:
const data = await response.json();
const formatted = data.items.map(item => ({
value: item.id,
label: item.name
}));
При быстром вводе пользователь может инициировать десятки запросов. Это приводит к гонке ответов. Решение — AbortController.
let controller = null;
element.addEventListener('search', async (event) => {
const query = event.detail.value;
if (controller) {
controller.abort();
}
controller = new AbortController();
try {
const response = await fetch(`/api/users?q=${query}`, {
signal: controller.signal
});
const data = await response.json();
choices.setChoices(
data.map(item => ({
value: item.id,
label: item.name
})),
'value',
'label',
true
);
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Ошибка загрузки:', err);
}
}
});
Такой подход гарантирует, что отображаются только актуальные результаты.
Даже с AbortController нагрузка на сервер может быть высокой. Поэтому добавляется задержка ввода:
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
Применение:
const searchUsers = debounce(async (query) => {
const response = await fetch(`/api/users?q=${query}`);
const data = await response.json();
choices.setChoices(
data.map(u => ({
value: u.id,
label: u.name
})),
'value',
'label',
true
);
}, 300);
element.addEventListener('search', (event) => {
searchUsers(event.detail.value);
});
Задержка в 200–400 мс является стандартной для UX-поиска.
При больших данных сервер часто возвращает результаты страницами:
{
"items": [...],
"page": 1,
"hasMore": true
}
Пример реализации:
let page = 1;
let currentQuery = '';
const loadPage = async (query, pageNum) => {
const response = await fetch(
`/api/users?q=${query}&page=${pageNum}`
);
return response.json();
};
Обработка:
element.addEventListener('search', async (event) => {
currentQuery = event.detail.value;
page = 1;
const data = await loadPage(currentQuery, page);
choices.setChoices(
data.items.map(i => ({
value: i.id,
label: i.name
})),
'value',
'label',
true
);
});
Для догрузки можно использовать кнопку “Показать ещё” или событие скролла списка.
Чтобы снизить количество запросов, используется локальный кэш:
const cache = new Map();
Логика:
const fetchUsers = async (query) => {
if (cache.has(query)) {
return cache.get(query);
}
const response = await fetch(`/api/users?q=${query}`);
const data = await response.json();
cache.set(query, data);
return data;
};
Такой подход особенно эффективен при повторяющихся запросах.
Метод setChoices перезаписывает данные. В некоторых
сценариях требуется добавление новых элементов:
choices.setChoices(
newItems,
'value',
'label',
false
);
Пятый параметр replaceChoices управляет поведением:
true — заменить полностью;false — добавить к существующим.При использовании fetch важно учитывать нестабильность соединения:
try {
const response = await fetch(`/api/users?q=${query}`);
if (!response.ok) {
throw new Error('HTTP ошибка');
}
const data = await response.json();
} catch (error) {
console.error('Ошибка запроса:', error);
choices.setChoices([
{
value: '',
label: 'Ошибка загрузки данных',
disabled: true
}
], 'value', 'label', true);
}
Это позволяет сохранить предсказуемость интерфейса.
При интеграции с backend часто требуется передавать дополнительные параметры:
const params = new URLSearchParams({
q: query,
limit: 20,
locale: 'ru'
});
fetch(`/api/users?${params.toString()}`);
Это обеспечивает совместимость с фильтрацией на сервере.
При работе с Choices.js и fetch API в масштабных системах применяются дополнительные техники:
Пример минимальной длины:
element.addEventListener('search', (event) => {
const query = event.detail.value;
if (query.length < 3) return;
searchUsers(query);
});
Если API требует токен, fetch дополняется заголовками:
fetch(`/api/users?q=${query}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
Choices.js при этом остаётся полностью независимым от механизма авторизации.
В реальных приложениях fetch-интеграция включает одновременно:
Пример комбинированного подхода:
let controller;
const cache = new Map();
const searchUsers = debounce(async (query) => {
if (query.length < 3) return;
if (cache.has(query)) {
choices.setChoices(cache.get(query), 'value', 'label', true);
return;
}
if (controller) controller.abort();
controller = new AbortController();
try {
const res = await fetch(`/api/users?q=${query}`, {
signal: controller.signal
});
const data = await res.json();
const formatted = data.map(u => ({
value: u.id,
label: u.name
}));
cache.set(query, formatted);
choices.setChoices(formatted, 'value', 'label', true);
} catch (e) {
if (e.name !== 'AbortError') {
console.error(e);
}
}
}, 300);
element.addEventListener('search', (e) => {
searchUsers(e.detail.value);
});