La API import.meta.hot: accept, dispose, data e invalidate
El contrato entre tu módulo y el runtime de HMR. accept declara un límite y recibe la nueva versión; dispose limpia efectos y guarda estado en data; invalidate rechaza la actualización y la propaga hacia arriba. Aceptar el propio módulo o un dependiente.
import.meta.hot es el asa que el módulo en marcha recibe sobre la maquinaria del HMR. A través de ella un módulo declara que puede absorber sus propias actualizaciones o las de una dependencia, guarda estado a través de los intercambios y, cuando descubre que no puede con el cambio, se rinde y deja que la onda siga subiendo. Cuatro métodos cargan con casi toda la API, y dominarlos desmitifica toda la magia de los frameworks.
- Usar
acceptpara declarar un límite: el propio módulo o un dependiente. - Preservar estado entre recargas con
disposeydata. - Rechazar una actualización con
invalidatey forzar la propagación hacia arriba. - Distinguir cuándo escribes esta API a mano y cuándo la inyecta un plugin.
accept: aceptar el propio módulo
accept() sin argumentos marca el módulo como autoaceptante: se declara a sí mismo un límite. Vite reimportará la nueva versión; si pasas un callback, este recibe el nuevo namespace del módulo para que reconectes lo que haga falta. Sin callback, Vite simplemente reejecuta el módulo entero, lo que basta para módulos con efectos como volver a pintar.
// aceptar el propio modulo, sin callback: se reejecuta al completo
if (import.meta.hot) {
import.meta.hot.accept();
}
// aceptar el propio modulo con callback: recibes el nuevo namespace
if (import.meta.hot) {
import.meta.hot.accept((nuevo) => {
if (nuevo) render(nuevo.estado);
});
}
El callback puede recibir undefined si la nueva versión lanzó una excepción al evaluarse; protégete de ese caso antes de usarlo.
accept: aceptar un dependiente
accept(dep, cb) —o accept([deps], cb) para varias— mantiene el módulo como el límite pero le hace absorber las actualizaciones de sus dependencias. El patrón típico: un módulo controlador acepta su dependencia de datos o de configuración y reacciona cuando cambia, sin reejecutarse él mismo por completo.
if (import.meta.hot) {
import.meta.hot.accept("./config.js", (nuevaConfig) => {
aplicarConfig(nuevaConfig.default);
});
import.meta.hot.accept(["./a.js", "./b.js"], ([a, b]) => {
// recibe un array alineado con el orden de las deps
});
}
La sutileza clave: la dependencia aceptada no reejecuta a sus propios importadores; el módulo que acepta asume toda la responsabilidad. Así es como localizas una actualización exactamente en el módulo que sabe cómo manejarla.
flowchart LR cambio[Editas el modulo] --> dispose[Corre dispose del modulo viejo] dispose --> data[Guarda estado en data] data --> nuevo[Se importa la nueva version] nuevo --> lee[Lee import.meta.hot.data] lee --> accept[Corre el callback de accept] style data fill:#f9e2af,color:#11111b style accept fill:#a6e3a1,color:#11111b
dispose y data: continuidad del estado
Antes de reemplazar un módulo, Vite llama a su handler dispose(cb). Úsalo para desmontar los efectos que creó la versión vieja —temporizadores, listeners, sockets, nodos del DOM, suscripciones—; si no, cada edición filtra una copia más. El callback recibe el objeto data.
import.meta.hot.data es un objeto que persiste a través del intercambio. La versión vieja escribe en data lo que debe sobrevivir, casi siempre dentro de dispose; la nueva lo lee de import.meta.hot.data al evaluarse. Ese es el mecanismo manual detrás de la “preservación de estado” que los frameworks automatizan.
if (import.meta.hot) {
// recupera el estado que viene de la version anterior
const previo = import.meta.hot.data.contador ?? 0;
let contador = previo;
const timer = setInterval(() => { contador++; }, 1000);
import.meta.hot.dispose((data) => {
clearInterval(timer); // limpia el efecto viejo
data.contador = contador; // guarda el estado para la nueva version
});
}
Sin la limpieza en dispose, cada guardado crearía un intervalo nuevo encima de los viejos: la fuga clásica del HMR escrito a mano.
Hay una sutileza de identidad que atrapa a muchos: import.meta.hot.data es el mismo objeto entre la versión vieja y la nueva, no una copia. Vite lo conserva a través del intercambio y se lo entrega a ambas. Por eso el patrón es escribir en él dentro de dispose y leerlo al evaluar el módulo nuevo: no hay serialización de por medio, solo una referencia que sobrevive a la discontinuidad, lo que te permite pasar incluso valores que no serían serializables, como una instancia viva o un nodo del DOM.
En los límites escritos a mano, el error más frecuente es no limpiar en dispose. Los efectos creados en el nivel superior del módulo —listeners, intervalos, servidores— se acumulan uno por edición porque los efectos del módulo viejo nunca se desmontan. La regla es simple y sin excepciones: cada accept que crea un efecto necesita un dispose que lo deshaga.
invalidate: rechazar y propagar
A veces un módulo acepta actualizaciones pero, en el momento de aplicar una concreta, descubre que no puede con ella —cambió el tipo de un export, una condición que no sabe reconciliar—. import.meta.hot.invalidate(mensaje) le dice a Vite: me rindo con esta actualización, trátame como si no aceptara y propaga a mis importadores. La propagación se reanuda desde este nodo hacia arriba, y puede acabar en un full reload. Es la válvula de escape que vuelve seguros los límites condicionales: aceptas de forma optimista e invalidas cuando el optimismo resultó equivocado.
if (import.meta.hot) {
import.meta.hot.accept((nuevo) => {
if (!puedoAplicar(nuevo)) {
// no puedo aplicar este cambio: que suba a mis importadores
import.meta.hot.invalidate("cambio incompatible");
return;
}
aplicar(nuevo);
});
}
Esto es exactamente lo que hace React Fast Refresh por debajo cuando un archivo deja de ser un límite de refresco válido —por ejemplo, al añadir un export que no es un componente—: invalida, y la actualización escala a los importadores o a una recarga completa.
Más allá de los cuatro grandes, import.meta.hot.on(evento, cb) y send te dejan escuchar y emitir eventos personalizados por el canal —útil para coordinar servidor y cliente—, y prune(cb) se dispara cuando un módulo deja de estar importado y se retira del grafo, el sitio ideal para una limpieza final. Son la periferia; el núcleo son accept, dispose, data e invalidate.
accept
Declara un límite. Sin deps, acepta el propio módulo; con deps, acepta a esos dependientes. El callback recibe la nueva versión.
dispose
Corre antes del intercambio. Desmonta los efectos de la versión vieja para que no se acumulen edición tras edición.
data
El objeto que persiste a través del intercambio. La versión vieja escribe en él; la nueva lo lee. Es el puente del estado.
invalidate
Rechaza la actualización y la propaga a los importadores. La admisión honesta de que este módulo no puede con el cambio.
Los cuatro métodos son un protocolo minúsculo, pero su forma es reveladora: entregan al módulo el control sobre su propia continuidad. accept responde a la pregunta puedo absorber esto; dispose responde qué debo limpiar y qué debe sobrevivir; data es el agujero de gusano por el que el estado cruza la discontinuidad de un intercambio de módulo; e invalidate es la admisión honesta de que la absorción falló. Lo elegante es que ese es el contrato entero. Todo lo que el HMR hace por ti, y todo lo que los frameworks construyen encima —Fast Refresh, el refresco de Solid, el HMR de stores y routers—, son estos cuatro primitivos compuestos. Escribirlos a mano una sola vez —un contador que sobrevive, un listener que no fuga, un límite que invalida cuando no puede— disuelve la magia de cualquier framework: el plugin no hace más que inyectar accept, dispose, data e invalidate alrededor de tus componentes para que tú nunca tengas que verlos. Y aclara los modos de fallo de un tirón: una fuga es un dispose que falta, un valor rancio es un data mal gestionado, una recarga sorpresa es un invalidate disparándose. La API es pequeña porque el problema, bien planteado, es pequeño: preservar la identidad y los efectos a través de una discontinuidad controlada en el grafo de módulos. Domina estos cuatro y el HMR deja de ser magia para volverse un mecanismo sobre el que razonas y que puedes extender.
- Escribe un límite de contador artesanal que sobreviva a las ediciones usando
dataydispose; verifica que la cuenta persiste. - Añade un
setIntervalsindispose, edita varias veces y observa la fuga —varios intervalos disparándose a la vez—; luego arréglala. - Convierte un autoaccept en un accept de dependencia: haz que un padre acepte
./config.jsy reaplique la config sin reejecutarse él mismo. - Añade una rama con
invalidateque se dispare ante un cambio incompatible y observa cómo la actualización escala hasta una recarga completa. - Registra un handler con
import.meta.hot.onpara un evento personalizado y emítelo consenddesde un plugin; observa la comunicación servidor-cliente por el canal.