Обработка ответов сервера

Обработка ответов сервера в Choices.js опирается на строгую нормализацию данных, единый контракт формата и управление асинхронными состояниями, поскольку библиотека изначально рассчитана на работу как со статическими списками, так и с удалёнными источниками данных. При интеграции с API ключевым становится этап преобразования произвольного ответа сервера в структуру, ожидаемую компонентом выбора.

Типовой ответ API для наполнения Choices.js представляет собой массив объектов, где каждый элемент описывает вариант выбора. Базовая структура ориентирована на минимальный набор полей:

[
  {
    "value": "1",
    "label": "Москва"
  },
  {
    "value": "2",
    "label": "Санкт-Петербург"
  }
]

Choices.js ожидает наличие как минимум value и label. Однако серверные ответы часто содержат расширенные данные:

[
  {
    "id": 10,
    "name": "Красный",
    "hex": "#ff0000",
    "available": true
  }
]

В таком случае требуется слой адаптации, преобразующий данные в формат библиотеки:

const normalize = (item) => ({
  value: item.id,
  label: item.name,
  disabled: !item.available,
  customProperties: {
    hex: item.hex
  }
});

Интеграция Choices.js с удалёнными данными

Choices.js не навязывает единственный способ загрузки данных, предоставляя возможность ручного управления через setChoices. На практике загрузка осуществляется через fetch, axios или любой другой HTTP-клиент.

const choices = new Choices('#select');

fetch('/api/cities')
  .then(res => res.json())
  .then(data => {
    const formatted = data.map(item => ({
      value: item.id,
      label: item.name
    }));

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

Метод setChoices принимает четыре параметра:

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

Такой подход позволяет полностью заменить содержимое списка при каждом ответе сервера.

Нормализация данных и адаптация структуры

Серверные ответы редко соответствуют UI-ожиданиям напрямую. Нормализация становится центральным этапом обработки.

Часто встречающиеся трансформации:

  • переименование ключей (idvalue, titlelabel)
  • фильтрация недоступных элементов
  • добавление вычисляемых полей
  • вложенные структуры

Пример обработки вложенных данных:

{
  "items": [
    {
      "meta": {
        "id": 5,
        "title": "Категория A"
      }
    }
  ]
}

Преобразование:

const transformResponse = (response) =>
  response.items.map(item => ({
    value: item.meta.id,
    label: item.meta.title
  }));

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

Асинхронная загрузка и управление состояниями

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

const choices = new Choices('#select', {
  loadingText: 'Загрузка...',
  noResultsText: 'Ничего не найдено',
  noChoicesText: 'Нет доступных вариантов'
});

Асинхронный процесс обычно включает три стадии:

  1. Инициация запроса
  2. Получение ответа
  3. Обновление списка

Пример управления состоянием:

choices.setChoices([], 'value', 'label', true);

fetch('/api/items')
  .then(res => res.json())
  .then(data => {
    choices.setChoices(
      data.map(x => ({ value: x.id, label: x.name })),
      'value',
      'label',
      true
    );
  });

Обработка ошибок сервера

Сетевые запросы требуют устойчивой обработки ошибок, поскольку Choices.js не управляет транспортным уровнем.

Базовый паттерн обработки:

fetch('/api/items')
  .then(res => {
    if (!res.ok) {
      throw new Error('Network error');
    }
    return res.json();
  })
  .then(data => {
    choices.setChoices(
      data.map(i => ({ value: i.id, label: i.name })),
      'value',
      'label',
      true
    );
  })
  .catch(() => {
    choices.clearChoices();
  });

Расширенные сценарии включают:

  • отображение fallback-значений
  • повтор запроса (retry policy)
  • логирование ошибок
  • деградацию интерфейса

Fallback-логика:

.catch(() => {
  choices.setChoices(
    [{ value: 'error', label: 'Данные недоступны', disabled: true }],
    'value',
    'label',
    true
  );
});

Пагинация и поэтапная подгрузка

При больших наборах данных сервер часто возвращает результаты порциями. Choices.js не реализует пагинацию автоматически, поэтому механизм строится поверх API.

Пример запроса с параметрами:

let page = 1;

const loadPage = () => {
  fetch(`/api/items?page=${page}`)
    .then(res => res.json())
    .then(data => {
      const formatted = data.items.map(i => ({
        value: i.id,
        label: i.name
      }));

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

Здесь важно отличие последнего параметра setChoices:

  • false позволяет добавлять элементы, а не заменять их

Это формирует основу для бесконечной прокрутки или догрузки при поиске.

Поиск на сервере и обработка query-параметров

Серверная фильтрация используется вместо клиентской при больших объёмах данных.

Типовой сценарий:

const search = (query) => {
  fetch(`/api/search?q=${encodeURIComponent(query)}`)
    .then(res => res.json())
    .then(data => {
      choices.setChoices(
        data.map(item => ({
          value: item.id,
          label: item.title
        })),
        'value',
        'label',
        true
      );
    });
};

Choices.js предоставляет событие поиска, которое может быть связано с этим запросом:

const choices = new Choices('#select', {
  searchEnabled: true
});

document.querySelector('#select').addEventListener('search', (e) => {
  search(e.detail.value);
});

При частых запросах применяется дебаунсинг:

let timer;

const debouncedSearch = (value) => {
  clearTimeout(timer);

  timer = setTimeout(() => {
    search(value);
  }, 300);
};

Кэширование и дедупликация запросов

Повторяющиеся запросы к серверу создают избыточную нагрузку. Кэширование решает проблему повторной загрузки одинаковых данных.

Простейшая реализация:

const cache = new Map();

const fetchCached = (url) => {
  if (cache.has(url)) {
    return Promise.resolve(cache.get(url));
  }

  return fetch(url)
    .then(res => res.json())
    .then(data => {
      cache.set(url, data);
      return data;
    });
};

Интеграция с Choices.js:

fetchCached('/api/items')
  .then(data => {
    choices.setChoices(
      data.map(i => ({ value: i.id, label: i.name })),
      'value',
      'label',
      true
    );
  });

Обработка сложных структур ответа

Некоторые API возвращают данные с метаинформацией, пагинацией и вложенными объектами одновременно:

{
  "meta": {
    "total": 100,
    "page": 1
  },
  "results": [
    {
      "id": 1,
      "title": "Элемент"
    }
  ]
}

Обработка:

const transform = (response) => response.results.map(item => ({
  value: item.id,
  label: item.title
}));

Дополнительные метаданные могут использоваться для управления UI:

if (response.meta.total > 100) {
  // включение режима пагинации
}

Динамическое обновление списка на основе состояния приложения

Choices.js позволяет многократно обновлять список без пересоздания инстанса. Это критично при работе с серверными источниками, где данные меняются в зависимости от контекста пользователя.

const updateChoices = async (params) => {
  const res = await fetch(`/api/items?filter=${params}`);
  const data = await res.json();

  choices.setChoices(
    data.map(i => ({
      value: i.id,
      label: i.name
    })),
    'value',
    'label',
    true
  );
};

Такой подход формирует реактивную модель: сервер становится источником истины, а Choices.js — только слоем отображения и взаимодействия.

Управление консистентностью данных

При асинхронных обновлениях возможны состояния гонки запросов. Более поздний запрос может завершиться раньше предыдущего, перезаписав актуальные данные.

Защита через токен запроса:

let requestId = 0;

const load = async () => {
  const currentId = ++requestId;

  const res = await fetch('/api/items');
  const data = await res.json();

  if (currentId !== requestId) return;

  choices.setChoices(
    data.map(i => ({ value: i.id, label: i.name })),
    'value',
    'label',
    true
  );
};

Такой механизм предотвращает устаревшие обновления интерфейса и обеспечивает согласованность отображаемых данных с последним запросом.