Saltar al contenido principal

Presentamos Hot Reloading

· 9 min de lectura
Martín Bigio
Ingeniero de Software en Instagram
Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

El objetivo de React Native es brindarte la mejor experiencia de desarrollo posible. Un aspecto crucial es el tiempo transcurrido entre guardar un archivo y poder ver los cambios. Nuestra meta es que este ciclo de retroalimentación sea inferior a 1 segundo, incluso a medida que tu aplicación crece.

Nos acercamos a este ideal mediante tres características principales:

  • Usar JavaScript, ya que este lenguaje no tiene tiempos largos de ciclo de compilación.

  • Implementar una herramienta llamada Packager que transforma archivos es6/flow/jsx en JavaScript estándar comprensible para la máquina virtual. Fue diseñado como servidor que mantiene estados intermedios en memoria para cambios incrementales rápidos y utiliza múltiples núcleos.

  • Crear una función llamada Live Reload que recarga la aplicación al guardar.

En este punto, el cuello de botella para desarrolladores ya no es el tiempo de recarga, sino la pérdida del estado de la aplicación. Un escenario común es trabajar en una función que está a varias pantallas de la de inicio. Cada vez que recargas, debes recorrer la misma ruta repetidamente para volver a tu función, alargando el ciclo a varios segundos.

Hot Reloading

La idea detrás de Hot Reloading es mantener la aplicación en ejecución e inyectar nuevas versiones de los archivos editados durante el tiempo de ejecución. Así no pierdes ningún estado, algo especialmente útil cuando ajustas la interfaz de usuario.

Un video vale más que mil palabras. Observa la diferencia entre Live Reload (actual) y Hot Reload (nuevo).

Si observas detenidamente, notarás que es posible recuperarse de un error (red box) e importar módulos que antes no existían sin necesidad de recarga completa.

Advertencia: como JavaScript es un lenguaje con estado, Hot Reloading no puede implementarse perfectamente. En la práctica, descubrimos que la configuración actual funciona bien para la mayoría de casos de uso comunes, y siempre está disponible la recarga completa si algo falla.

Hot Reloading está disponible desde la versión 0.22. Puedes activarlo así:

  • Abre el menú de desarrollo

  • Toca "Enable Hot Reloading"

Implementación en pocas palabras

Ahora que vimos por qué lo necesitamos y cómo usarlo, viene la parte divertida: cómo funciona realmente.

Hot Reloading se basa en Hot Module Replacement (HMR), una función introducida primero por webpack que implementamos en React Native Packager. HMR hace que Packager detecte cambios en archivos y envíe actualizaciones HMR a un pequeño entorno de ejecución HMR incluido en la app.

En esencia, la actualización HMR contiene el nuevo código de los módulos JS modificados. Cuando el entorno de ejecución los recibe, reemplaza el código antiguo por el nuevo:

La actualización HMR contiene algo más que solo el código del módulo porque reemplazarlo no basta para que el entorno de ejecución detecte cambios. El problema es que el sistema de módulos podría tener en caché las exportaciones del módulo a actualizar. Por ejemplo, imagina una app con estos dos módulos:

// log.js
function log(message) {
const time = require('./time');
console.log(`[${time()}] ${message}`);
}

module.exports = log;
// time.js
function time() {
return new Date().getTime();
}

module.exports = time;

El módulo log imprime el mensaje recibido incluyendo la fecha actual proporcionada por el módulo time.

Al agrupar la app, React Native registra cada módulo mediante la función __d. Para esta app, entre muchas definiciones __d, habrá una para log:

__d('log', function() {
... // module's code
});

Esta invocación envuelve el código de cada módulo en una función anónima, comúnmente llamada función fábrica. El sistema de módulos mantiene un registro de la función fábrica de cada módulo, si ya ha sido ejecutada y el resultado de dicha ejecución (exportaciones). Cuando un módulo es requerido, el sistema proporciona las exportaciones en caché o ejecuta la función fábrica por primera vez guardando el resultado.

Imagina que inicias tu aplicación y requieres log. En este punto, ni log ni las funciones fábrica de time han sido ejecutadas, por lo que no hay exportaciones en caché. Luego, el usuario modifica time para retornar la fecha en formato MM/DD:

// time.js
function bar() {
const date = new Date();
return `${date.getMonth() + 1}/${date.getDate()}`;
}

module.exports = bar;

El Packager enviará el nuevo código de time al runtime (paso 1), y cuando log sea requerido eventualmente, la función exportada se ejecutará incorporando los cambios de time (paso 2):

Ahora supongamos que el código de log requiere time como una dependencia de primer nivel:

const time = require('./time'); // top level require

// log.js
function log(message) {
console.log(`[${time()}] ${message}`);
}

module.exports = log;

Cuando se requiere log, el runtime almacenará en caché sus exportaciones y las de time (paso 1). Si luego modificas time, el proceso HMR no puede finalizar simplemente reemplazando el código de time. Si lo hiciera, cuando se ejecute log, usaría la copia en caché de time (código antiguo).

Para que log incorpore los cambios de time, debemos limpiar sus exportaciones en caché porque uno de sus módulos dependientes fue reemplazado en caliente (paso 3). Finalmente, cuando log sea requerido nuevamente, su función fábrica se ejecutará requiriendo time y obteniendo su nuevo código.

API de HMR

HMR en React Native extiende el sistema de módulos mediante el objeto hot. Esta API está basada en la de webpack. El objeto hot expone una función accept que permite definir un callback ejecutado cuando el módulo necesita ser reemplazado en caliente. Por ejemplo, si modificamos el código de time así, veremos "time changed" en consola cada vez que guardemos:

// time.js
function time() {
... // new code
}

module.hot.accept(() => {
console.log('time changed');
});

module.exports = time;

Nota: Solo en casos raros necesitarás usar esta API manualmente. Hot Reloading debería funcionar automáticamente en la mayoría de los casos comunes.

Runtime de HMR

Como vimos antes, a veces no basta con aceptar la actualización HMR porque un módulo que usa el componente reemplazado podría ya haberse ejecutado con sus importaciones en caché. Por ejemplo, supongamos que la app de películas tiene un MovieRouter de primer nivel que depende de las vistas MovieSearch y MovieScreen, las cuales dependen de los módulos log y time:

Si el usuario accede a la vista de búsqueda pero no a otras, todos los módulos excepto MovieScreen tendrían exportaciones en caché. Al cambiar time, el runtime deberá limpiar las exportaciones de log para incorporar los cambios de time. El proceso no termina ahí: el runtime repetirá esto recursivamente hasta que todos los padres hayan sido aceptados. Buscará módulos que dependan de log e intentará aceptarlos. Para MovieScreen puede omitirse (no se ha requerido). Para MovieSearch, limpiará sus exportaciones y procesará sus padres recursivamente. Finalmente, hará lo mismo con MovieRouter y terminará, ya que ningún módulo depende de él.

Para recorrer el árbol de dependencias, el runtime recibe el árbol de dependencias inversas del Packager en la actualización HMR. Para este ejemplo recibirá un objeto JSON como:

{
modules: [
{
name: 'time',
code: /* time's new code */
}
],
inverseDependencies: {
MovieRouter: [],
MovieScreen: ['MovieRouter'],
MovieSearch: ['MovieRouter'],
log: ['MovieScreen', 'MovieSearch'],
time: ['log'],
}
}

Componentes de React

Los componentes de React son un poco más difíciles de hacer funcionar con Hot Reloading. El problema es que no podemos simplemente reemplazar el código antiguo por el nuevo porque perderíamos el estado del componente. Para aplicaciones web de React, Dan Abramov implementó una transformación de Babel que usa la API HMR de webpack para resolver este problema. En resumen, su solución funciona creando un proxy para cada componente React durante el tiempo de transformación. Los proxies mantienen el estado del componente y delegan los métodos del ciclo de vida en los componentes reales, que son los que recargamos en caliente:

Además de crear el componente proxy, la transformación también define la función accept con un fragmento de código para forzar a React a volver a renderizar el componente. Así podemos recargar en caliente el código de renderizado sin perder el estado de la aplicación.

El transformador predeterminado que viene con React Native usa babel-preset-react-native, que está configurado para usar react-transform de la misma manera que lo harías en un proyecto web de React que utilice webpack.

Tiendas Redux

Para habilitar Hot Reloading en tiendas Redux, solo necesitarás usar la API HMR de manera similar a como lo harías en un proyecto web con webpack:

// configureStore.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import reducer from '../reducers';

export default function configureStore(initialState) {
const store = createStore(
reducer,
initialState,
applyMiddleware(thunk),
);

if (module.hot) {
module.hot.accept(() => {
const nextRootReducer = require('../reducers/index').default;
store.replaceReducer(nextRootReducer);
});
}

return store;
};

Cuando cambias un reducer, el código para aceptar ese reducer se enviará al cliente. Entonces el cliente se dará cuenta de que el reducer no sabe cómo aceptarse a sí mismo, por lo que buscará todos los módulos que lo referencian e intentará aceptarlos. Eventualmente, el flujo llegará a la tienda única, el módulo configureStore, que aceptará la actualización HMR.

Conclusión

Si estás interesado en ayudar a mejorar el hot reloading, te animo a leer la publicación de Dan Abramov sobre el futuro del hot reloading y a contribuir. Por ejemplo, Johny Days va a hacerlo funcionar con múltiples clientes conectados. Dependemos de todos ustedes para mantener y mejorar esta función.

Con React Native, tenemos la oportunidad de repensar cómo construimos aplicaciones para ofrecer una gran experiencia de desarrollo. El hot reloading es solo una pieza del rompecabezas, ¿qué otras soluciones creativas podemos implementar para mejorarla?