Отложенная инициализация

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

Одной из основных причин отложенной инициализации является отсутствие данных на момент загрузки страницы. Например, список опций может приходить с сервера после выполнения HTTP-запроса.

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

  • элемент <select> уже присутствует в DOM;
  • данные для заполнения опций получены.
import Choices from 'choices.js';

const sel ect = document.querySelector('#country-select');

fetch('/api/countries')
  .then(response => response.json())
  .then(data => {
    const choices = new Choices(select, {
      searchEnabled: true,
      shouldSort: false
    });

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

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

Инициализация после загрузки DOM

Во многих случаях элемент существует в разметке, но скрипт выполняется раньше, чем DOM полностью построен. Это особенно актуально при подключении JavaScript в <head> без defer.

Отложенная инициализация через событие DOMContentLoaded решает эту проблему.

import Choices fr om 'choices.js';

document.addEventListener('DOMContentLoaded', () => {
  const element = document.querySelector('#city-select');

  const choices = new Choices(element, {
    removeItemButton: true,
    searchPlaceholderValue: 'Поиск...'
  });
});

Такой подход гарантирует наличие элемента в DOM и предотвращает попытки инициализации на null.

Ленивая инициализация (Lazy Init)

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

import Choices from 'choices.js';

let cityChoices = null;

const citySelect = document.querySelector('#city-select');

function initCitySelect() {
  if (cityChoices) return;

  cityChoices = new Choices(citySelect, {
    searchEnabled: true
  });
}

citySelect.addEventListener('focus', initCitySelect, { once: true });

В этом варианте экземпляр создаётся только при первом фокусе на элементе. Это снижает начальную нагрузку на страницу, особенно при наличии десятков селектов.

Инициализация в SPA-архитектуре

В приложениях с динамической отрисовкой (React-подобные или собственные SPA) DOM может пересоздаваться. В таких условиях отложенная инициализация должна учитывать жизненный цикл компонента.

import Choices from 'choices.js';

function mountSelect(container) {
  const sel ect = container.querySelector('select');

  const instance = new Choices(select, {
    shouldSort: false
  });

  return instance;
}

let instance = null;

function renderView() {
  const container = document.querySelector('#view');

  container.innerHTML = `
    <select id="tags">
      <option value="js">JavaScript</option>
      <option value="py">Python</option>
    </select>
  `;

  instance = mountSelect(container);
}

При каждом повторном рендере необходимо учитывать уничтожение предыдущего экземпляра:

if (instance) {
  instance.destroy();
  instance = null;
}

Игнорирование этого шага приводит к утечкам памяти и дублированию DOM-структур.

Инициализация после динамического добавления элементов

Choices.js не отслеживает появление новых элементов автоматически. Если <select> добавляется в DOM после загрузки страницы, требуется явная инициализация.

import Choices fr om 'choices.js';

function addSelect() {
  const sel ect = document.createElement('select');
  select.id = 'dynamic-select';

  select.innerHTML = `
    <option value="1">Первый</option>
    <option value="2">Второй</option>
  `;

  document.body.appendChild(select);

  new Choices(select, {
    searchEnabled: false
  });
}

В этом случае важен порядок операций: сначала вставка в DOM, затем инициализация.

Отложенная инициализация с загрузкой через API и повторным обновлением

Иногда Choices создаётся пустым, а данные подгружаются позже. Тогда используется обновление через setChoices.

import Choices fr om 'choices.js';

const sel ect = document.querySelector('#product-select');

const choices = new Choices(select, {
  shouldSort: false
});

async function loadProducts() {
  const res = await fetch('/api/products');
  const data = await res.json();

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

loadProducts();

Такой вариант удобен, когда визуальная структура должна быть доступна сразу, но данные загружаются постепенно.

Использование MutationObserver для отложенного подключения

В сложных интерфейсах, где элементы могут появляться из сторонних скриптов, можно использовать наблюдение за DOM.

import Choices fr om 'choices.js';

const observer = new MutationObserver(() => {
  const sel ect = document.querySelector('#async-select');

  if (select && !select.dataset.initialized) {
    select.dataset.initialized = 'true';

    new Choices(select, {
      searchEnabled: true
    });
  }
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

Маркер dataset.initialized предотвращает повторную инициализацию одного и того же элемента.

Управление жизненным циклом экземпляра

Отложенная инициализация требует строгого контроля состояния экземпляра Choices.js. Основные операции включают:

  • создание экземпляра;
  • обновление данных;
  • уничтожение экземпляра.
let instance = null;

function init(select) {
  if (instance) {
    instance.destroy();
  }

  instance = new Choices(select, {
    removeItemButton: true
  });
}

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

Типичные ошибки отложенной инициализации

Одной из распространённых проблем является повторная инициализация одного и того же элемента без вызова destroy. Это приводит к:

  • дублированию UI;
  • некорректной обработке событий;
  • увеличению потребления памяти.

Другой частой ошибкой является попытка инициализации до появления элемента:

const el = document.querySelector('#select');
new Choices(el); // el может быть null

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

Комбинированные сценарии

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

import Choices fr om 'choices.js';

let choicesInstance = null;

async function ensureInit() {
  const select = document.querySelector('#combo-select');

  if (!select) return;

  if (!choicesInstance) {
    choicesInstance = new Choices(select, {
      searchEnabled: true
    });

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

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

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