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 важно различать два подхода:
Choices.js полностью управляет состоянием DOM. React не синхронизирует выбранные значения.
React хранит состояние выбора, а Choices синхронизируется через API экземпляра.
const [value, setValue] = useState([]);
useEffect(() => {
if (!instance) return;
instance.setChoiceByValue(value);
}, [value]);
Такой подход требует аккуратной синхронизации:
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" }
]
Важно избегать повторной инициализации библиотеки при каждом изменении данных, иначе теряется состояние и производительность резко падает.
Интеграция с формами требует мостика между 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)setChoicesconst 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), иначе запросы перегружают сервер.
Choices.js поставляется с базовыми стилями, но в React-проектах часто требуется кастомизация:
.choicesclassNames в конфигурацииnew Choices(selectRef.current, {
classNames: {
containerOuter: "custom-choices",
input: "custom-input",
},
});
Типизация упрощает работу с экземпляром:
import Choices fr om "choices.js";
const instance = useRef<Choices | null>(null);
Типы для событий обычно приходится расширять вручную:
type ChoiceValue = string | number;
Причина: отсутствие пустого dependency array в
useEffect.
Причина: отсутствие destroy() при размонтировании
компонента.
Причина: одновременное управление DOM и React без явного источника истины.
Choices.js не работает на сервере, поэтому требуется динамический импорт:
useEffect(() => {
import("choices.js").then((module) => {
const Choices = module.default;
instance.current = new Choices(selectRef.current);
});
}, []);
На практике Choices.js лучше рассматривать как изолированный imperative-слой:
Такое разделение снижает связность и упрощает поддержку, особенно в больших формах и админ-панелях.