React компонент

Работа с нативными DOM-библиотеками в React требует явного контроля жизненного цикла и аккуратного управления состоянием, поскольку React не отслеживает изменения, происходящие вне его виртуального DOM. Tom Sel ect в этом контексте выступает как внешняя императивная система, которую необходимо корректно синхронизировать с React-компонентами.

Базовая стратегия интеграции

Основной подход заключается в создании контролируемой React-обёртки, внутри которой инициализируется экземпляр Tom Sel ect. Вся работа с библиотекой происходит через ref, поскольку прямое взаимодействие с DOM-узлом является обязательным условием.

Ключевая идея:

  • React управляет состоянием (value, options)
  • Tom Select управляет UI-рендерингом
  • синхронизация происходит через эффекты (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-обёртке важно учитывать:

  • отмену запросов при размонтировании
  • предотвращение race conditions
  • кеширование запросов при необходимости

Использование AbortController

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 важно помнить:

  • HTML формируется вне React-дерева
  • обновление UI не синхронизировано с React reconciliation
  • требуется аккуратная очистка данных

Управление фокусом и поведением UI

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]);

При больших списках важно избегать:

  • частых clearOptions
  • полной пересборки DOM
  • лишних setValue

Обработка событий

Tom 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");
});

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

SSR и Next.js особенности

При серверном рендеринге 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-узла.

Расширенная архитектура обёртки

При масштабных приложениях компонент часто разделяется на уровни:

  • UI слой (React компонент)
  • слой адаптера (инициализация Tom Select)
  • слой данных (fetch, cache, transform)

Такой подход позволяет:

  • переиспользовать конфигурации
  • централизовать обработку ошибок
  • отделить UI от логики загрузки данных

Типизация (TypeScript)

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);

Частые архитектурные ошибки

  • повторная инициализация Tom Select при изменении props
  • отсутствие очистки при unmount
  • конфликт controlled/uncontrolled режима
  • прямое изменение DOM вне Tom Select API
  • несинхронизированные значения value

Каждая из этих проблем приводит к рассинхронизации UI и React-состояния, что выражается в визуальных артефактах и неконсистентном поведении компонента