Работа в React

Choices.js в React-приложениях используется как обёртка над нативными <select>-элементами, но с расширенной логикой: поиском, мультивыбором, тегированием и кастомной отрисовкой элементов. При интеграции с React ключевая сложность заключается в конфликте подходов: библиотека напрямую манипулирует DOM, тогда как React управляет виртуальным DOM.

В React доступ к DOM-узлу осуществляется через useRef, а инициализация экземпляра Choices выполняется внутри useEffect, чтобы гарантировать наличие элемента в DOM.

import { useEffect, useRef } from "react";
import Choices from "choices.js";
import "choices.js/public/assets/styles/choices.min.css";

export default function SelectComponent() {
  const selectRef = useRef(null);

  useEffect(() => {
    const instance = new Choices(selectRef.current, {
      searchEnabled: true,
      removeItemButton: true,
      shouldSort: false,
    });

    return () => {
      instance.destroy();
    };
  }, []);

  return (
    <sel ect ref={selectRef} multiple>
      <option value="js">JavaScript</option>
      <option value="ts">TypeScript</option>
      <option value="react">React</option>
    </select>
  );
}

Ключевой момент — обязательное уничтожение экземпляра через destroy(), иначе при повторных рендерах остаются «висящие» обработчики событий и дублирующиеся DOM-узлы.

Управляемые и неуправляемые сценарии

При работе с React важно различать два подхода:

Неуправляемый (imperative)

Choices.js полностью управляет состоянием DOM. React не синхронизирует выбранные значения.

  • проще интеграция
  • выше риск рассинхронизации
  • подходит для статических форм

Управляемый (controlled)

React хранит состояние выбора, а Choices синхронизируется через API экземпляра.

const [value, setValue] = useState([]);

useEffect(() => {
  if (!instance) return;
  instance.setChoiceByValue(value);
}, [value]);

Такой подход требует аккуратной синхронизации:

  • обновление React → обновление Choices
  • изменение Choices → обновление React через события

Подписка на события Choices.js

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

useEffect(() => {
  const instance = new Choices(selectRef.current);

  const handleChange = (event) => {
    setValue(instance.getValue(true));
  };

  selectRef.current.addEventListener("change", handleChange);

  return () => {
    selectRef.current.removeEventListener("change", handleChange);
    instance.destroy();
  };
}, []);

Основные события:

  • change — изменение выбранных значений
  • addItem — добавление элемента
  • removeItem — удаление элемента
  • search — ввод в поле поиска

Динамическое обновление списка

В React часто данные приходят асинхронно. В этом случае экземпляр Choices нужно обновлять через API, а не пересоздавать:

useEffect(() => {
  if (!instance) return;

  instance.clearStore();
  instance.setChoices(
    options,
    "value",
    "label",
    true
  );
}, [options]);

Где options имеет формат:

[
  { value: "js", label: "JavaScript" },
  { value: "react", label: "React" }
]

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

Использование с React Hook Form

Интеграция с формами требует мостика между DOM-библиотекой и системой регистрации полей.

import { useForm, Controller } fr om "react-hook-form";

<Controller
  name="skills"
  control={control}
  render={({ field }) => (
    <sel ect
      ref={(el) => {
        field.ref(el);
        selectRef.current = el;
      }}
      multiple
    />
  )}
/>

После инициализации Choices:

useEffect(() => {
  const instance = new Choices(selectRef.current);

  instance.passedElement.element.addEventListener("change", () => {
    field.onChange(instance.getValue(true));
  });

  return () => instance.destroy();
}, []);

Обработка больших списков

При работе с тысячами опций основная проблема — производительность DOM-рендеринга.

Рекомендации:

  • отключать сортировку (shouldSort: false)
  • использовать серверный поиск вместо локального
  • ограничивать количество отображаемых элементов
  • применять lazy-loading через setChoices
const fetchOptions = async (query) => {
  const res = await fetch(`/api/search?q=${query}`);
  const data = await res.json();
  instance.setChoices(data, "value", "label", true);
};

Асинхронный поиск

Choices.js поддерживает кастомную обработку поиска через событие search:

useEffect(() => {
  const instance = new Choices(selectRef.current, {
    searchEnabled: true,
  });

  const searchHandler = async (event) => {
    const query = event.detail.value;

    const res = await fetch(`/api/tags?q=${query}`);
    const data = await res.json();

    instance.setChoices(data, "value", "label", true);
  };

  selectRef.current.addEventListener("search", searchHandler);

  return () => instance.destroy();
}, []);

При этом важно учитывать задержку ввода (debounce), иначе запросы перегружают сервер.

Стилизация в React-проектах

Choices.js поставляется с базовыми стилями, но в React-проектах часто требуется кастомизация:

  • переопределение CSS классов .choices
  • использование CSS Modules или styled-components
  • контроль через classNames в конфигурации
new Choices(selectRef.current, {
  classNames: {
    containerOuter: "custom-choices",
    input: "custom-input",
  },
});

TypeScript-интеграция

Типизация упрощает работу с экземпляром:

import Choices fr om "choices.js";

const instance = useRef<Choices | null>(null);

Типы для событий обычно приходится расширять вручную:

type ChoiceValue = string | number;

Частые проблемы интеграции

1. Повторная инициализация

Причина: отсутствие пустого dependency array в useEffect.

2. Утечки памяти

Причина: отсутствие destroy() при размонтировании компонента.

3. Несинхронизированное состояние

Причина: одновременное управление DOM и React без явного источника истины.

4. SSR (Next.js)

Choices.js не работает на сервере, поэтому требуется динамический импорт:

useEffect(() => {
  import("choices.js").then((module) => {
    const Choices = module.default;
    instance.current = new Choices(selectRef.current);
  });
}, []);

Архитектурный подход в React-приложениях

На практике Choices.js лучше рассматривать как изолированный imperative-слой:

  • React отвечает за данные и состояние
  • Choices.js отвечает за UI select-виджета
  • мост между ними реализуется через ref и события

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