Асинхронная загрузка через AJAX

Архитектура динамического источника данных

Choices.js изначально проектируется как компонент для управления списками <select> и <input> с возможностью расширенного поиска и кастомизации. При работе с удалёнными источниками данных библиотека не содержит встроенного «магического AJAX-режима» в стиле старых UI-фреймворков — вместо этого используется модель внешнего управления состоянием через программные методы экземпляра.

Ключевой принцип асинхронной интеграции заключается в том, что Choices.js не выполняет запросы самостоятельно, а предоставляет API для:

  • динамического добавления опций
  • полной замены списка
  • управления поиском и отображением
  • реакции на пользовательский ввод

Это делает интеграцию с любым API (REST, GraphQL, JSON-RPC) унифицированной.


Инициализация компонента для удалённого источника

Базовая конфигурация предполагает включение поиска и отключение статического набора значений:

const element = document.querySelector('#city-select');

const choices = new Choices(element, {
  searchEnabled: true,
  shouldSort: false,
  placeholderValue: 'Поиск...',
  removeItemButton: true
});

На этом этапе компонент готов принимать данные извне, но список опций пока пуст или статичен.


Перехват пользовательского ввода

Основной механизм реакции на запросы пользователя — событие search. Оно позволяет перехватывать ввод и запускать асинхронный запрос:

element.addEventListener('search', async (event) => {
  const query = event.detail.value;

  if (query.length < 2) return;

  const results = await fetchCities(query);
  choices.setChoices(results, 'value', 'label', true);
});

Здесь:

  • event.detail.value — текущая строка поиска
  • fetchCities — функция обращения к серверу
  • setChoices — полная замена текущего списка

Запрос к серверу через fetch

Типовая реализация AJAX-слоя строится на fetch:

async function fetchCities(query) {
  const response = await fetch(`/api/cities?search=${encodeURIComponent(query)}`);

  if (!response.ok) {
    throw new Error('Ошибка загрузки данных');
  }

  const data = await response.json();

  return data.items.map(item => ({
    value: item.id,
    label: item.name
  }));
}

Ключевая задача слоя преобразования — нормализация ответа API в формат Choices.js:

{
  value: 'unique-id',
  label: 'Отображаемое название'
}

Полная замена списка vs инкрементальное обновление

Метод setChoices поддерживает режимы обновления:

choices.setChoices(data, 'value', 'label', true);

Последний аргумент:

  • true — полностью очищает старые значения
  • false — добавляет к существующим

При работе с AJAX почти всегда используется полная замена, чтобы избежать дублирования результатов при каждом запросе.


Дебаунсинг запросов

Без ограничения частоты ввода асинхронные запросы могут перегрузить сервер. Используется debounce:

function debounce(fn, delay) {
  let timer;

  return function (...args) {
    clearTimeout(timer);

    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

Применение:

const handleSearch = debounce(async (value) => {
  if (value.length < 2) return;

  const data = await fetchCities(value);
  choices.setChoices(data, 'value', 'label', true);
}, 300);

element.addEventListener('search', (e) => {
  handleSearch(e.detail.value);
});

Отмена предыдущих запросов (AbortController)

При быстром вводе возможна гонка ответов, когда старый запрос перезаписывает новый результат. Решение — отмена предыдущего fetch:

let controller = null;

async function fetchCities(query) {
  if (controller) {
    controller.abort();
  }

  controller = new AbortController();

  const response = await fetch(`/api/cities?search=${query}`, {
    signal: controller.signal
  });

  const data = await response.json();

  return data.items.map(item => ({
    value: item.id,
    label: item.name
  }));
}

Это гарантирует актуальность данных в UI.


Кэширование результатов

Для уменьшения нагрузки на сервер используется локальный кэш:

const cache = new Map();

async function fetchCities(query) {
  if (cache.has(query)) {
    return cache.get(query);
  }

  const response = await fetch(`/api/cities?search=${query}`);
  const data = await response.json();

  const formatted = data.items.map(item => ({
    value: item.id,
    label: item.name
  }));

  cache.set(query, formatted);

  return formatted;
}

При больших данных кэш можно ограничивать по размеру или времени жизни.


Обработка пустых и ошибочных ответов

Асинхронная интеграция требует явной обработки крайних случаев:

try {
  const results = await fetchCities(query);

  if (!results.length) {
    choices.clearStore();
    return;
  }

  choices.setChoices(results, 'value', 'label', true);
} catch (error) {
  choices.clearStore();
}

Метод clearStore используется для очистки списка опций.


Интеграция с внешними API с задержкой ответа

При работе с медленными API важно учитывать UX:

  • отображение состояния загрузки
  • предотвращение мерцания списка
  • сохранение предыдущих значений до получения новых

Пример:

element.addEventListener('search', async (e) => {
  const query = e.detail.value;

  choices.setChoices([{ value: '', label: 'Загрузка...', disabled: true }], 'value', 'label', true);

  const data = await fetchCities(query);

  choices.setChoices(data, 'value', 'label', true);
});

Фильтрация данных на стороне клиента после AJAX

Иногда сервер возвращает избыточный набор данных, требующий дополнительной фильтрации:

const filtered = data.items
  .filter(item => item.population > 100000)
  .map(item => ({
    value: item.id,
    label: item.name
  }));

Такая стратегия снижает необходимость в сложных серверных параметрах.


Пагинация результатов

Для больших справочников применяется постраничная загрузка:

let page = 1;
let queryState = '';

async function loadMore(query) {
  const response = await fetch(`/api/cities?search=${query}&page=${page}`);
  const data = await response.json();

  const formatted = data.items.map(item => ({
    value: item.id,
    label: item.name
  }));

  choices.setChoices(formatted, 'value', 'label', page === 1);

  page++;
  queryState = query;
}

При изменении запроса page сбрасывается.


Связка с внешними состояниями приложения

Choices.js в AJAX-режиме часто используется как часть более широкой архитектуры:

  • Redux / Zustand / Vuex для хранения выбранных значений
  • сервисный слой API
  • централизованный HTTP-клиент (axios/fetch wrapper)

Пример синхронизации:

choices.passedElement.element.addEventListener('change', (e) => {
  store.setState({
    cityId: e.detail.value
  });
});

Типовая ошибка: конкурирующие setChoices

Частая проблема — параллельные вызовы setChoices, приводящие к «миганию» данных.

Решение — контроль последовательности:

let requestId = 0;

async function handleSearch(query) {
  const currentId = ++requestId;

  const data = await fetchCities(query);

  if (currentId !== requestId) return;

  choices.setChoices(data, 'value', 'label', true);
}

Оптимальная схема AJAX-интеграции

Комбинация всех практик:

  • debounce ввода
  • AbortController
  • кэширование
  • защита от гонок
  • нормализация API
  • обработка пустых состояний

формирует устойчивую архитектуру асинхронного селекта:

const cache = new Map();
let controller = null;
let requestId = 0;

Эта модель позволяет масштабировать компонент до десятков тысяч записей без деградации UX.