Time travel: qué significa realmente viajar por el historial
Retroceder y avanzar por el historial de acciones es la demostración más vistosa de Redux y también la peor entendida. Esta lección desmonta la ilusión: las DevTools no rebobinan el programa ni deshacen nada, simplemente vuelven a plegar el mismo reducer sobre el estado inicial con un prefijo distinto de la lista de acciones, y la interfaz obedece porque es una función pura del estado. De ahí salen sus dos capacidades reales —saltar a cualquier instante y desactivar acciones concretas para ver el mundo sin ellas— y su límite estructural: los efectos ya ocurrieron y ninguna recomputación los cancela. Cierra separando con precisión lo que sí vuelve atrás, lo que no vuelve nunca y por qué esa frontera coincide exactamente con la frontera entre lo puro y lo impuro.
La demostración que vendió Redux a una generación entera dura quince segundos: alguien arrastra un deslizador hacia atrás y la interfaz retrocede sola, estado por estado, como si el tiempo obedeciera. La palabra elegida para nombrarlo —viaje en el tiempo— es a la vez brillante y tramposa, porque sugiere que el programa vuelve atrás cuando en realidad nada vuelve atrás nunca. Entender qué ocurre de verdad bajo esa animación es lo que separa a quien usa la función de quien sabe cuándo puede confiar en ella. Y la respuesta es prosaica y hermosa: no hay viaje, hay recálculo. Todo el resto de esta lección son las consecuencias de esa frase.
- Explicar el mecanismo real del salto temporal: replegar el reducer sobre un prefijo distinto del historial.
- Distinguir viajar en el tiempo de deshacer, y entender por qué solo el primero es gratis.
- Usar el desactivado selectivo de acciones para probar hipótesis sobre el origen de un bug.
- Delimitar con precisión qué revierte el salto y qué queda fuera de su alcance por ser impuro.
No hay viaje, hay recálculo
La afirmación central es simple: las DevTools no guardan una máquina del tiempo, guardan una lista. Tienen el estado inicial de la aplicación y la secuencia ordenada de todas las acciones despachadas desde entonces. Para mostrarte el mundo tal como estaba en el instante número siete, no rebobinan nada: toman el estado inicial y vuelven a aplicar el reducer sobre las siete primeras acciones, exactamente igual que la primera vez. El resultado es idéntico porque el reducer es puro, y es idéntico siempre, tantas veces como quieras repetirlo.
Las implementaciones reales optimizan ese pliegue guardando fotografías intermedias para no recalcular desde cero en cada movimiento del deslizador, pero la optimización no cambia el modelo: sigue siendo válido pensar que cada estado se obtiene aplicando el reducer a un prefijo del historial, y esa es la imagen mental que hay que retener.
Dicho de otro modo, el estado en cualquier punto del historial no es un dato que se recuperó: es un valor que se recalculó a demanda mediante un pliegue de la lista de acciones. Esta es la razón profunda por la que la pureza del reducer no era una manía académica. Si el reducer consultase la hora, generara un identificador aleatorio o llamara a la red, replegarlo daría un resultado distinto cada vez y la vista del pasado sería una ficción diferente en cada intento.
// El estado de cualquier instante es un pliegue del historial. Sin magia.
function estadoEn(indice: number): Estado {
return historial
.slice(0, indice)
.reduce((acc, accion) => reducer(acc, accion), estadoInicial)
}
Basta una impureza mínima para que la reconstrucción deje de ser fiel, y el ejemplo canónico cabe en dos líneas:
// Rompe el pliegue: el mismo historial da un resultado distinto cada vez.
function reducer(state: Estado, accion: Accion): Estado {
if (accion.type === 'nota/crear') {
return { ...state, ultima: { id: crypto.randomUUID(), en: Date.now() } }
}
return state
}
Al replegar mañana ese historial obtendrás otro identificador y otra marca temporal, así que el pasado que la extensión te enseña no es el pasado que ocurrió sino una versión nueva de él. La lección tercera de este nivel dedica una sección entera a mover esas dos impurezas al borde, porque son las mismas que rompen la reproducción de sesiones. Conviene retener ya la forma general: cada valor que el reducer inventa en lugar de recibir es un punto por donde el historial pierde fidelidad.
Que la interfaz siga al estado sin resistirse es la otra mitad del truco, y también estaba pagada de antemano. Una vista de React es una función del estado: dale el estado del instante siete y dibujará la pantalla del instante siete, sin saber ni preguntar si ese estado es el actual o uno reconstruido. La combinación de un reducer puro y una vista pura convierte al historial en la única fuente de verdad y al presente en un caso particular del pasado: el último.
Esa última frase no es un adorno retórico y conviene tomársela literalmente, porque explica por qué el mecanismo no necesita ningún modo especial. La aplicación no distingue entre estar viva y estar siendo inspeccionada: en ambos casos recibe un estado y dibuja. Cuando arrastras el deslizador no entras en un modo de depuración con reglas propias, simplemente le entregas a la misma función de vista un argumento distinto del que le habría tocado. Por eso los componentes no necesitan saber nada de todo esto, y por eso la funcionalidad no se degrada a medida que la aplicación crece: no hay maquinaria adicional que mantener sincronizada.
Conviene separar dos operaciones que se confunden a diario. Viajar en el tiempo es una herramienta de desarrollo: recalcula la vista para que tú la mires, y no modifica el historial ni pretende que el usuario final lo use. Deshacer es una funcionalidad del producto: el usuario espera que la última operación quede realmente anulada, incluida su consecuencia en el servidor. Redux te regala el primero por su modelo y no te regala el segundo, aunque le ponga fácil implementarlo. Confundirlos lleva a la expectativa equivocada de que el deslizador es el botón de deshacer que le falta a tu producto, y no lo es ni pretende serlo. Un botón de deshacer real necesita decidir qué hacer con los efectos ya emitidos, y esa decisión es de dominio, no de librería.
Desactivar acciones: el experimento contrafactual
La segunda capacidad, menos vistosa y mucho más útil, es poder desactivar una acción concreta en mitad del historial y ver qué mundo resulta sin ella. El mecanismo es el mismo pliegue, con la lista filtrada: se recorre el historial saltándose la acción marcada y se recalcula todo lo posterior. Lo que obtienes es un experimento contrafactual sobre tu propia sesión, algo que en la mayoría de las arquitecturas exigiría reproducir la situación a mano.
Es importante ver que esta capacidad es estrictamente más fuerte que la anterior. Saltar a un instante te enseña un pasado que efectivamente ocurrió; desactivar una acción te enseña un pasado que no ocurrió nunca, un mundo alternativo calculado a partir del mismo reducer. Que un depurador pueda mostrarte con fidelidad algo que no pasó es una consecuencia nada obvia de haber hecho el estado recomputable, y no hay ninguna herramienta equivalente para código que muta.
flowchart TD I[estado inicial] --> A1[accion 1] A1 --> A2[accion 2] A2 --> A3[accion 3 desactivada] A3 --> A4[accion 4] A4 --> R[estado recalculado sin la 3] I -.mismo reducer.-> R style A3 fill:#f38ba8,color:#11111b style R fill:#a6e3a1,color:#11111b
El valor diagnóstico es directo. Si sospechas que cierta acción es la culpable de un estado corrupto, la desactivas: si el bug desaparece, tu hipótesis se confirma sin escribir una línea de código; si persiste, has eliminado un sospechoso y puedes seguir. Es bisección aplicada al historial, y su coste por experimento es un clic, frente a los minutos que costaría reproducir la misma pregunta comentando código y recargando.
La cautela adicional es que desactivar solo tiene sentido con acciones independientes. Si la acción que quitas era la que creaba la entidad sobre la que operan las siguientes, el pliegue resultante no es un mundo alternativo coherente sino uno imposible, y las conclusiones que saques de él no valen. Antes de interpretar el resultado, comprueba que las acciones posteriores siguen teniendo sentido sin la que eliminaste. La única cautela es recordar que la respuesta que obtienes es sobre el estado, no sobre el mundo: si aquella acción había disparado también una escritura remota, desactivarla en las DevTools no deshace la escritura, solo el efecto que tuvo sobre tu árbol local.
Saltar a un instante
Recalcula el estado con un prefijo del historial y repinta la vista. Reversible, repetible y sin coste sobre el sistema real.
Desactivar una acción
Repliega el historial sin ella para ver el mundo alternativo. Confirma o descarta una hipótesis sin tocar el código.
Repetir tras editar
Cambias el reducer, la extensión vuelve a plegar el mismo historial y la vista refleja la corrección al instante.
Fijar el punto de partida
Marcas un estado como nuevo inicio y descartas lo anterior para acotar el historial a la zona que investigas.
La técnica de acotar por mitades te la sabes aplicada a los commits. Aplícala aquí a las acciones: si el estado ya está mal en el instante veinte y estaba bien en el cero, salta al diez y mira. Con cinco saltos localizas la acción exacta que introdujo la corrupción en un historial de treinta, sin leer una línea de código y sin repetir la interacción. El deslizador no es un juguete de demostración: es el instrumento de búsqueda binaria más rápido que tienes sobre el pasado de tu aplicación, y usarlo así convierte la depuración por conjetura en una búsqueda con final garantizado.
La tercera tarjeta merece una nota aparte porque es la que más productividad aporta y la que menos se usa. Con el historial fijo, editar el reducer y guardar hace que las DevTools vuelvan a plegar exactamente las mismas acciones sobre el código nuevo. Tienes así un bucle de prueba y corrección donde la entrada permanece constante y solo cambia la función bajo estudio, que es la condición ideal de cualquier experimento y algo muy difícil de conseguir a mano.
La cuarta tarjeta, fijar el punto de partida, cumple una función distinta y complementaria: descarta el historial anterior y declara el estado actual como nuevo origen. Es la operación menos vistosa de las cuatro y la que más se agradece en una investigación larga. Sirve para acotar sesiones largas —llegas hasta el punto donde empieza lo interesante, fijas, y a partir de ahí trabajas con un historial corto y legible— y también para no arrastrar durante horas el arranque de la aplicación en cada pliegue. Es la operación que convierte una sesión de trabajo real en un experimento manejable.
El límite: los efectos ya ocurrieron
Aquí termina la magia y empieza la honestidad. El pliegue recalcula estado; no cancela nada de lo que salió de tu proceso. Si en el instante cinco tu aplicación envió una petición que borró un registro en la base de datos, retroceder al instante cuatro devuelve tu árbol local a como estaba, y el registro sigue borrado. Lo mismo vale para el correo enviado, el pago cursado, la analítica registrada, la entrada escrita en localStorage o el mensaje emitido por un socket.
La frontera es exacta y vale la pena enunciarla con precisión: vuelve atrás todo lo que es función pura del historial, y no vuelve nada de lo que fue observado por el exterior. Esa frontera coincide punto por punto con la que separaba reducers de middleware en el Nivel 4. Los efectos viven en el middleware precisamente porque no son replegables, y el time travel funciona sobre los reducers precisamente porque sí lo son. La arquitectura no tiene aquí una limitación accidental; tiene una consecuencia deducible de su propio diseño.
Esa frontera, además, es una herramienta de diseño y no solo una advertencia. Cuando dudes de dónde colocar una pieza de lógica, pregúntate si querrías que volviera atrás con el deslizador. Si la respuesta es sí, esa lógica pertenece al reducer y debe ser pura; si es no, pertenece al borde y su sitio es el middleware. Es el mismo criterio del Nivel 4 formulado desde el otro lado, y tiene la ventaja de ser una pregunta concreta y contestable en lugar de una discusión abstracta sobre pureza.
Hay un matiz que muerde en la práctica. Si tu efecto vive en un middleware o en un listener que reacciona a un tipo de acción, y las DevTools vuelven a emitir esas acciones al recalcular, puedes acabar disparando la petición otra vez. Las herramientas modernas evitan la mayoría de estos casos porque replegar solo pasa las acciones por el reducer y no por toda la cadena, pero las configuraciones caseras y ciertos middleware con estado interno propio se rompen. La señal de alarma es cualquier middleware que guarde estado fuera del store: al replegar, ese estado no se recalcula y tu reconstrucción del pasado deja de ser fiel.
Existe también un límite de recursos que se olvida por lo obvio. El historial completo se guarda en memoria, y en una sesión larga con acciones frecuentes crece hasta hacer lenta la propia extensión. Las DevTools limitan por defecto el número de acciones retenidas y permiten ajustarlo, además de filtrar por tipo para excluir el ruido de alta frecuencia. Un historial acotado y legible vale más que uno completo e inmanejable. La última lección de este nivel vuelve sobre este coste con números y añade la razón menos técnica y más seria para no retenerlo todo.
Conviene además distinguir dos clases de irreversibilidad que se confunden. Hay efectos que salieron y no vuelven porque otro sistema los observó —el correo, el pago, la escritura remota— y hay estado que no vuelve porque nunca estuvo en el historial: un valor guardado en una variable de módulo, un contador dentro de una clase, una referencia mutable escondida en un hook. El primer caso es una limitación honesta del modelo y se acepta. El segundo es un defecto que se puede corregir, y el deslizador es justo el instrumento que lo delata: si al retroceder la interfaz no vuelve del todo a su estado anterior, hay estado viviendo fuera del store y acabas de encontrar exactamente dónde.
Si tu producto necesita deshacer de verdad, el modelo de Redux te da una base excelente y no la solución. La vía habitual es guardar en el estado una pila de estados anteriores o de acciones inversas, mediante una librería como redux-undo o a mano, y exponerla como parte del dominio. La parte que ninguna librería resuelve por ti es la compensación de los efectos: deshacer una publicación exige emitir una despublicación, no simplemente olvidar que se publicó. Ese patrón de acciones compensatorias es viejo y bien conocido en sistemas distribuidos, y es exactamente lo que separa un deshacer de juguete de uno que resiste el contacto con un servidor.
El mismo teorema fuera del navegador
Merece la pena sacar esta idea del contexto de una extensión de Chrome, porque la estructura que la sostiene es mucho más vieja y mucho más general que Redux. Cuando decides que la verdad de un sistema no es su estado actual sino la secuencia de hechos que lo produjeron, y que el estado se obtiene plegando esa secuencia, has adoptado el patrón que en el mundo de los sistemas de información se llama registro de eventos. Redux es una implementación diminuta de esa idea aplicada a la interfaz de usuario, y el time travel es la propiedad que cualquier sistema con esa forma obtiene gratis.
Verlo así reordena la genealogía de la herramienta. Redux no inventó nada en este terreno: tomó una estructura conocida desde hacía décadas en contabilidad, en bases de datos y en control de versiones, y la aplicó a un dominio donde nadie la había aplicado, el de la interfaz de un navegador. Su aportación real fue de traducción y de divulgación, no de invención, y eso explica que el modelo haya sobrevivido a la librería concreta que lo popularizó.
El control de versiones distribuido guarda una secuencia de cambios y reconstruye cualquier árbol de trabajo replegándola, que es por qué puedes situarte en cualquier punto del pasado sin haber guardado una copia de cada versión. Un libro contable no borra un asiento equivocado: escribe otro que lo compensa, para que el saldo siga siendo un pliegue de la historia completa. Una base de datos con registro de escritura anticipada recupera su estado tras una caída replegando ese registro. En los tres casos, la capacidad de reconstruir el pasado no se añadió como funcionalidad: se dedujo de haber decidido que los hechos, y no el resumen, eran la verdad del sistema.
Reconocer esa familia tiene un valor práctico inmediato, porque te da el vocabulario para saber qué esperar de cada diseño antes de construirlo. Si el estado de un módulo es un pliegue determinista de sus entradas, tendrás reconstrucción del pasado, reproducción y verificación sobre entrada fija; si no lo es, no las tendrás por mucho que instales herramientas. Y a la inversa: cuando alguien te pida en un producto una funcionalidad de auditoría, de deshacer o de replicar la sesión de un usuario, lo que en realidad está pidiendo es que el módulo tenga esta forma, y decidirlo al principio cuesta una tarde mientras que introducirlo después cuesta un rediseño.
La comparación también marca las diferencias, y conviene no exagerar el parentesco. Un registro de eventos de servidor es duradero, ordenado globalmente y pensado para sobrevivir décadas; el historial de Redux vive en memoria, muere al recargar la pestaña y solo ordena lo que ocurrió en un cliente. Redux no es un sistema de eventos, es un sistema con la forma de uno, y esa forma le basta para obtener en el ámbito reducido de una interfaz las mismas tres propiedades: reconstrucción, auditoría y reproducción. Saber que la analogía es de estructura y no de escala evita tanto despreciarla como pedirle garantías que nunca ofreció.
Lo que de verdad enseña esta lección no es cómo mover un deslizador, sino que la posibilidad misma de moverlo es una demostración. Si tu aplicación admite viaje en el tiempo, has probado que el estado es una función pura del historial de intenciones y que la vista es una función pura del estado; y si no lo admite, has probado que en algún punto de tu sistema alguien guarda información que no está en esa cadena. El deslizador es, literalmente, un verificador de la propiedad arquitectónica más importante que Redux te pedía, y por eso cuando deja de funcionar la respuesta correcta no es reparar la extensión sino buscar dónde se filtró el estado oculto. De ahí se sigue algo que casi nadie dice en voz alta: el time travel fue histórica y pedagógicamente decisivo no porque la gente lo usara todos los días —no lo hace— sino porque hizo visible, en quince segundos de animación, una idea abstracta que llevaba décadas escrita en los papeles de la programación funcional. La comunidad no se enamoró de una utilidad de depuración; se enamoró de ver con sus propios ojos que un programa interactivo podía ser un pliegue determinista, y esa imagen reordenó una década de arquitecturas de interfaz, incluidas todas las de la ola ligera que hoy se presentan como su alternativa. Y precisamente porque es un teorema y no un truco, su límite también es informativo: que los efectos no vuelvan atrás no es un defecto de la implementación, es la frontera exacta entre lo que tu programa puede reconstruir y lo que el mundo ya observó. Aprender a leer esa frontera —esto lo recalculo, esto ya salió y es irreversible— es una de las distinciones más útiles que te llevarás de todo el track, porque reaparece intacta en las máquinas de estado, en el registro de eventos de un sistema distribuido y en cualquier diseño donde alguien pretenda deshacer algo que otro ya vio.
- Reproduce en tu app un flujo de al menos quince acciones y recorre el historial completo hacia atrás. Anota en qué punto exacto la interfaz deja de parecerse a lo que recordabas.
- Elige la acción que creas responsable de un comportamiento raro y desactívala. Registra si el bug desaparece y qué te dice esa respuesta sobre tu hipótesis.
- Sin cerrar la extensión, corrige el reducer sospechoso y guarda. Comprueba que el mismo historial se repliega sobre el código nuevo y evalúa el resultado sin repetir la interacción a mano.
- Localiza en tu app una acción que dispare un efecto externo. Retrocede hasta antes de ella y verifica en el servidor o en el almacenamiento que ese efecto sigue ahí. Escribe la frase que describe qué volvió y qué no.
- Busca cualquier middleware que mantenga estado propio fuera del
store. Repliega el historial y comprueba si su reconstrucción es fiel; si no lo es, mueve ese estado alstorey repite. - Sube el volumen de acciones hasta que la extensión se vuelva lenta. Aplica un filtro por tipo o limita el historial retenido y compara la legibilidad del monitor antes y después.