Работа с нативными DOM-библиотеками в React требует явного контроля жизненного цикла и аккуратного управления состоянием, поскольку React не отслеживает изменения, происходящие вне его виртуального DOM. Tom Sel ect в этом контексте выступает как внешняя императивная система, которую необходимо корректно синхронизировать с React-компонентами.
Основной подход заключается в создании контролируемой React-обёртки,
внутри которой инициализируется экземпляр Tom Sel ect. Вся работа с
библиотекой происходит через ref, поскольку прямое
взаимодействие с DOM-узлом является обязательным условием.
Ключевая идея:
value,
options)useEffect)import React, { useEffect, useRef } fr om "react";
import TomSelect fr om "tom-select";
import "tom-select/dist/css/tom-select.css";
export default function TomSelectInput({
options = [],
value,
onChange,
placeholder = "Выбор..."
}) {
const selectRef = useRef(null);
const tomSelectRef = useRef(null);
useEffect(() => {
if (!selectRef.current) return;
tomSelectRef.current = new TomSelect(selectRef.current, {
options,
valueField: "value",
labelField: "label",
searchField: "label",
placeholder,
onChange: (val) => {
onChange?.(val);
}
});
return () => {
tomSelectRef.current?.destroy();
tomSelectRef.current = null;
};
}, []);
return (
<sel ect ref={selectRef} defaultValue={value}>
{options.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
}
В React предпочтительнее использовать контролируемый подход, при котором значение синхронизируется через props. Однако Tom Select не является нативно реактивным, поэтому требуется ручная синхронизация.
При изменении value извне Tom Select не обновляется
автоматически. Для решения используется дополнительный
useEffect.
useEffect(() => {
if (!tomSelectRef.current) return;
if (value !== tomSelectRef.current.getValue()) {
tomSelectRef.current.setValue(value, true);
}
}, [value]);
Второй аргумент true предотвращает повторное
срабатывание события onChange, избегая бесконечных циклов
обновления.
Динамические данные требуют полного или частичного пересоздания списка опций.
useEffect(() => {
if (!tomSelectRef.current) return;
tomSelectRef.current.clearOptions();
tomSelectRef.current.addOptions(options);
}, [options]);
При сложных сценариях (например, фильтрация на сервере) часто используется стратегия:
Tom Select поддерживает lazy-loading через кастомный
load метод.
tomSelectRef.current = new TomSelect(selectRef.current, {
valueField: "id",
labelField: "title",
searchField: "title",
load: (query, callback) => {
if (!query.length) return callback();
fetch(`/api/search?q=${encodeURIComponent(query)}`)
.then(res => res.json())
.then(data => callback(data))
.catch(() => callback());
}
});
В React-обёртке важно учитывать:
useEffect(() => {
const controller = new AbortController();
tomSelectRef.current = new TomSelect(selectRef.current, {
load: (query, callback) => {
fetch(`/api/items?q=${query}`, {
signal: controller.signal
})
.then(r => r.json())
.then(data => callback(data))
.catch(() => callback());
}
});
return () => {
controller.abort();
tomSelectRef.current?.destroy();
};
}, []);
Tom Select активно используется как replacement multi-select компонентов.
tomSelectRef.current = new TomSelect(selectRef.current, {
maxItems: null,
plugins: ["remove_button"]
});
React-обёртка при этом должна корректно обрабатывать массив значений:
useEffect(() => {
if (!tomSelectRef.current) return;
tomSelectRef.current.setValue(value || []);
}, [value]);
Важно учитывать, что value всегда должен быть массивом,
даже если пустым.
Tom Select поддерживает шаблоны рендера через
render.
tomSelectRef.current = new TomSelect(selectRef.current, {
render: {
option: (data, escape) => {
return `<div>
<strong>${escape(data.label)}</strong>
<div class="hint">${escape(data.description || "")}</div>
</div>`;
},
item: (data, escape) => {
return `<div>${escape(data.label)}</div>`;
}
}
});
При использовании в React важно помнить:
React часто перерисовывает компоненты, что может сбивать фокус внутри Tom Select. Для контроля используются методы:
tomSelectRef.current.focus();
tomSelectRef.current.blur();
Также можно управлять открытием dropdown:
tomSelectRef.current.open();
tomSelectRef.current.close();
Частая ошибка при интеграции — пересоздание Tom Select при каждом рендере компонента. Решается через:
useEffect([])useRef вместо state для инстансаconst memoOptions = React.useMemo(() => options, [options]);
При больших списках важно избегать:
clearOptionssetValueTom Select предоставляет богатый набор событий, которые можно использовать внутри React:
tomSelectRef.current.on("change", (value) => {
onChange?.(value);
});
tomSelectRef.current.on("item_add", () => {
console.log("item added");
});
tomSelectRef.current.on("item_remove", () => {
console.log("item removed");
});
Важный момент: события следует навешивать один раз при инициализации, иначе возможны дубли.
При серверном рендеринге Tom Select должен инициализироваться только на клиенте.
useEffect(() => {
if (typeof window === "undefined") return;
tomSelectRef.current = new TomSelect(selectRef.current, {});
}, []);
В Next.js часто используется динамический импорт:
import dynamic fr om "next/dynamic";
const TomSelectInput = dynamic(() => import("./TomSelectInput"), {
ssr: false
});
Корректное уничтожение инстанса критично для предотвращения утечек памяти:
return () => {
tomSelectRef.current?.destroy();
tomSelectRef.current = null;
};
При этом destroy() должен вызываться всегда при
размонтировании компонента или пересоздании DOM-узла.
При масштабных приложениях компонент часто разделяется на уровни:
Такой подход позволяет:
type Option = {
value: string;
label: string;
};
interface Props {
options: Option[];
value: string | string[];
onChange?: (value: string | string[]) => void;
placeholder?: string;
}
Дополнительно можно типизировать инстанс:
const tomSelectRef = useRef<TomSelect | null>(null);
valueКаждая из этих проблем приводит к рассинхронизации UI и React-состояния, что выражается в визуальных артефактах и неконсистентном поведении компонента