HMR API: module.hot.accept, module.hot.dispose

Hot Module Replacement (HMR) — механизм, позволяющий обновлять отдельные модули приложения во время разработки без полной перезагрузки страницы. Вместо повторной загрузки всего приложения изменённый модуль подменяется на лету, сохраняя текущее состояние интерфейса, данные форм, содержимое переменных и другую информацию, находящуюся в памяти.

В Parcel поддержка HMR встроена по умолчанию и не требует отдельной настройки. При запуске сервера разработки Parcel отслеживает изменения файлов и автоматически инициирует обновление соответствующих модулей.

Для управления процессом обновления используется объект module.hot, предоставляющий набор методов жизненного цикла HMR. Наиболее важными являются:

  • module.hot.accept() — обработка обновления модуля;
  • module.hot.dispose() — очистка состояния перед заменой модуля.

Эти методы позволяют не просто принимать обновления, а полностью контролировать сохранение и восстановление состояния приложения.


Объект module.hot

При работе в режиме разработки Parcel внедряет в каждый модуль специальный объект:

module.hot

Наличие этого объекта означает, что модуль поддерживает горячую замену.

Обычно код HMR заключается в условную проверку:

if (module.hot) {
  // HMR логика
}

Это предотвращает ошибки в окружениях, где HMR отсутствует, например при production-сборке.


Метод module.hot.accept()

Назначение

Метод accept() сообщает Parcel, что модуль способен самостоятельно обработать своё обновление без полной перезагрузки страницы.

Простейший вариант:

if (module.hot) {
  module.hot.accept();
}

После сохранения файла Parcel заменит текущую версию модуля новой.


Как работает accept()

Рассмотрим модуль:

export function getMessage() {
  return "Версия 1";
}

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

import { getMessage } from "./message";

console.log(getMessage());

if (module.hot) {
  module.hot.accept();
}

После изменения:

export function getMessage() {
  return "Версия 2";
}

Parcel загружает новую версию модуля без полной перезагрузки страницы.


accept с колбэком

Часто после обновления необходимо выполнить дополнительную логику.

Для этого используется функция обратного вызова:

if (module.hot) {
  module.hot.accept(() => {
    console.log("Модуль обновлён");
  });
}

Каждый раз после замены модуля колбэк будет вызван автоматически.


Перерисовка интерфейса после обновления

Предположим, имеется функция рендера:

export function render() {
  document.body.innerHTML = `
    <h1>Привет, Parcel</h1>
  `;
}

Главный модуль:

import { render } from "./render";

render();

if (module.hot) {
  module.hot.accept(() => {
    render();
  });
}

После изменения содержимого функции render() страница будет обновляться без полной перезагрузки.


Повторный импорт обновлённого модуля

Во многих случаях требуется получить новую версию зависимого модуля.

Например:

import { render } from "./render";

render();

if (module.hot) {
  module.hot.accept(async () => {
    const module = await import("./render");

    module.render();
  });
}

Здесь после HMR-заменены выполняется динамический импорт актуальной версии файла.


HMR и сохранение состояния

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

Рассмотрим пример:

let counter = 0;

export function increment() {
  counter++;
  console.log(counter);
}

После обновления файла значение:

counter

снова станет равно нулю.

Для сохранения данных используется механизм передачи состояния между версиями модуля.


Метод module.hot.dispose()

Назначение

Метод dispose() вызывается непосредственно перед удалением старой версии модуля.

Он позволяет:

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

Синтаксис:

module.hot.dispose((data) => {
  // действия перед удалением
});

Параметр data представляет объект для передачи информации новой версии модуля.


Жизненный цикл HMR

При изменении файла происходит следующая последовательность:

  1. Обнаружение изменения.
  2. Вызов обработчиков dispose.
  3. Удаление старого модуля.
  4. Загрузка нового кода.
  5. Выполнение нового модуля.
  6. Вызов обработчиков accept.

Схематично процесс выглядит так:

Старая версия
      │
      ▼
dispose()
      │
      ▼
Удаление модуля
      │
      ▼
Загрузка нового кода
      │
      ▼
accept()
      │
      ▼
Новая версия

Передача данных между версиями модуля

Сохранение состояния

Исходный модуль:

let counter = 0;

export function increment() {
  counter++;
  console.log(counter);
}

Перед удалением:

if (module.hot) {
  module.hot.dispose((data) => {
    data.counter = counter;
  });
}

Теперь значение будет записано в объект состояния.


Восстановление состояния

Новая версия модуля может получить данные через:

module.hot.data

Пример:

let counter = module.hot?.data?.counter || 0;

export function increment() {
  counter++;
  console.log(counter);
}

Полный код:

let counter = module.hot?.data?.counter || 0;

export function increment() {
  counter++;
  console.log(counter);
}

if (module.hot) {
  module.hot.dispose((data) => {
    data.counter = counter;
  });

  module.hot.accept();
}

Теперь значение счётчика сохраняется между обновлениями.


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

Одна из наиболее распространённых задач для dispose() — удаление подписок и слушателей.

Неправильный вариант:

button.addEventListener("click", handler);

После нескольких HMR-обновлений количество обработчиков может увеличиться.

В результате один клик приведёт к множественным вызовам.


Правильная очистка

const handler = () => {
  console.log("Click");
};

button.addEventListener("click", handler);

if (module.hot) {
  module.hot.dispose(() => {
    button.removeEventListener("click", handler);
  });

  module.hot.accept();
}

Перед заменой старый обработчик удаляется.


Очистка таймеров

Часто модули запускают интервалы:

const timer = setInterval(() => {
  console.log("tick");
}, 1000);

Без очистки после нескольких обновлений появятся десятки активных таймеров.

Использование dispose():

const timer = setInterval(() => {
  console.log("tick");
}, 1000);

if (module.hot) {
  module.hot.dispose(() => {
    clearInterval(timer);
  });

  module.hot.accept();
}

Очистка WebSocket-соединений

Создание соединения:

const socket = new WebSocket("ws://localhost:8080");

Перед заменой:

if (module.hot) {
  module.hot.dispose(() => {
    socket.close();
  });

  module.hot.accept();
}

Это предотвращает накопление лишних соединений после множества изменений кода.


Работа с пользовательским состоянием

Иногда необходимо сохранить сложную структуру данных.

Пример:

let store = {
  theme: "dark",
  language: "ru",
  notifications: true
};

Сохранение:

module.hot.dispose((data) => {
  data.store = store;
});

Восстановление:

let store = module.hot?.data?.store || {
  theme: "dark",
  language: "ru",
  notifications: true
};

Использование нескольких dispose-обработчиков

Допускается регистрация нескольких обработчиков:

module.hot.dispose(() => {
  clearInterval(timer);
});

module.hot.dispose(() => {
  socket.close();
});

module.hot.dispose(() => {
  console.log("Очистка завершена");
});

Все функции будут вызваны перед заменой модуля.


Использование accept и dispose совместно

Наиболее распространённый шаблон выглядит следующим образом:

let state = module.hot?.data?.state || {};

initialize();

if (module.hot) {
  module.hot.dispose((data) => {
    data.state = state;

    cleanup();
  });

  module.hot.accept(() => {
    initialize();
  });
}

Здесь реализованы сразу три задачи:

  1. Сохранение состояния.
  2. Освобождение ресурсов.
  3. Повторная инициализация после обновления.

Практический пример с состоянием формы

Допустим, имеется поле ввода:

<input id="name" />

Получение значения:

const input = document.querySelector("#name");

Сохранение перед заменой:

if (module.hot) {
  module.hot.dispose((data) => {
    data.value = input.value;
  });
}

Восстановление:

if (module.hot?.data?.value) {
  input.value = module.hot.data.value;
}

После изменения кода введённый текст останется в поле.


Практический пример с пользовательским хранилищем

let state = module.hot?.data?.state || {
  count: 0
};

document.body.innerHTML = `
  <button id="btn">${state.count}</button>
`;

const button = document.querySelector("#btn");

button.addEventListener("click", () => {
  state.count++;

  button.textContent = state.count;
});

if (module.hot) {
  module.hot.dispose((data) => {
    data.state = state;
  });

  module.hot.accept();
}

Даже после сохранения файла текущее значение счётчика не потеряется.


Ограничения HMR

Не все изменения могут быть применены через горячую замену.

Полная перезагрузка страницы обычно происходит при:

  • изменении конфигурации сборщика;
  • изменении HTML-шаблонов, требующих полной пересборки;
  • изменении зависимостей, нарушающих HMR-цепочку;
  • возникновении ошибок выполнения во время обновления.

В таких случаях Parcel автоматически выполняет reload страницы.


Рекомендации по использованию

Использовать dispose для любых внешних ресурсов

К внешним ресурсам относятся:

  • WebSocket;
  • EventSource;
  • DOM-события;
  • таймеры;
  • подписки на состояния;
  • подключения к сторонним библиотекам.

Любой ресурс, создаваемый модулем, должен быть освобождён перед его удалением.


Хранить только необходимые данные

Объект:

module.hot.data

предназначен для временной передачи состояния между двумя версиями одного модуля.

Не следует помещать туда:

  • большие коллекции данных;
  • бинарные объекты;
  • кеши значительных размеров;
  • DOM-элементы.

Лучше сохранять только минимальный набор данных, необходимый для восстановления состояния.


Проверять наличие HMR

Все вызовы должны выполняться через проверку:

if (module.hot) {
  module.hot.accept();
}

или

if (module.hot) {
  module.hot.dispose(() => {});
}

Такой подход гарантирует корректную работу как в режиме разработки, так и в production-сборке.


Типовой шаблон HMR-модуля

let state = module.hot?.data?.state || {
  value: 0
};

function init() {
  console.log("Инициализация");
}

function destroy() {
  console.log("Очистка");
}

init();

if (module.hot) {
  module.hot.dispose((data) => {
    data.state = state;

    destroy();
  });

  module.hot.accept(() => {
    init();
  });
}

Данный шаблон отражает базовую архитектуру работы с HMR в Parcel: сохранение состояния через module.hot.dispose(), восстановление данных через module.hot.data и принятие обновлений посредством module.hot.accept(). Именно такая комбинация обеспечивает быструю разработку без полной перезагрузки приложения и потери пользовательского состояния.