wandres.dev
REDUX DEVTOOLS · time travel real

Qué hacen las DevTools: acción, diff e inmutabilidad

Las Redux DevTools no son un visor de estado más: son la consecuencia visible de haber aceptado que todo cambio pase por una acción serializable y que ningún reducer mute lo que recibe. Esta lección abre la extensión por dentro y explica qué pregunta responde cada panel —qué pasó, cómo quedó el mundo, qué cambió exactamente, quién lo pidió— y por qué el diff, el historial y la reproducción solo son calculables cuando cada estado anterior sigue existiendo intacto en memoria. Cierra con el inventario de prácticas que degradan la observabilidad sin llegar a romper la app: mutar dentro de un reducer, colar un valor no serializable en el estado, o despachar acciones cuyo nombre no dice nada de la intención del usuario.

⏱ 18 min

Toda la ceremonia de los niveles anteriores —la acción como objeto plano, el reducer puro, la prohibición de mutar— tuvo un precio que ya pagaste. Este nivel es donde se cobra el dividendo. Las Redux DevTools son la herramienta que convirtió una disciplina austera en algo que se ve, y su poder no viene de la extensión sino del contrato: si cada cambio del mundo es un objeto con nombre, y si cada estado intermedio sobrevive porque nadie lo sobrescribió, entonces el historial completo de tu aplicación es un dato inspeccionable. Lo que sigue no es un tutorial de una extensión de navegador; es el estudio de qué se puede saber de un programa cuando su forma de cambiar fue diseñada para ser observable.

🎯 Al terminar esta lección sabrás
  • Leer el monitor de acciones como el registro de decisiones que ninguna otra arquitectura regala gratis.
  • Interpretar los paneles Action, State, Diff y Trace, y saber qué pregunta distinta responde cada uno.
  • Explicar por qué el diff y el historial solo son calculables si cada reducer devuelve un estado nuevo.
  • Reconocer las prácticas que degradan la observabilidad: mutar, guardar valores no serializables, nombrar mal.

El registro que no tuviste que escribir

En una aplicación construida con mutación libre, saber por qué el mundo llegó a un estado concreto exige reconstruirlo: poner console.log, adivinar el orden, correlacionar eventos. En una aplicación Redux ese registro ya existe, y existe sin que nadie lo escribiera, porque escribirlo era la única forma permitida de cambiar algo. Las DevTools no instrumentan tu código: se limitan a suscribirse a un flujo que ya estaba ahí y a guardar cada acción junto con el estado que produjo.

El enganche técnico es diminuto. La extensión expone un objeto global en la página; el store lo detecta al crearse y le envía cada par de acción y estado resultante. En Redux Toolkit ese cableado viene activado por defecto en desarrollo, de modo que la primera vez que abres la pestaña ya hay historia dentro. La consecuencia conceptual es más interesante que la mecánica: la extensión no es un depurador que interrumpe la ejecución, sino un observador que acumula. No pone puntos de ruptura ni congela el proceso; recibe una secuencia de hechos ya ocurridos y te deja recorrerla a tu ritmo.

Lo que viaja por ese canal es más modesto de lo que la interfaz sugiere. En esencia, una entrada del historial es un par: el objeto despachado y el árbol resultante, ambos datos planos.

type Entrada = {
  accion: { type: string; payload?: unknown }
  estado: unknown
  ts: number
}

Todo lo que la extensión sabe hacer se calcula a partir de esa lista de pares. No hay ningún canal privilegiado hacia tu código, ninguna instrumentación del compilador, ningún acceso al ámbito de tus funciones: hay una secuencia de valores que tu propio modelo generó y que alguien tuvo la buena idea de no tirar a la basura.

ℹ️
No es un depurador, es un archivo

La diferencia con el depurador del navegador es de naturaleza, no de grado. Un depurador te da acceso al presente: detiene la ejecución y te enseña el estado vivo en ese instante. Las DevTools de Redux te dan acceso al pasado completo, sin detener nada, porque cada estado intermedio se guardó en vez de destruirse. Por eso conviven bien: el depurador contesta qué está pasando ahora dentro de esta función, y las DevTools contestan cómo llegamos hasta aquí desde que arrancó la app. La segunda pregunta es la que más tiempo consume en un bug real, y es la que casi ninguna otra arquitectura sabe responder sin trabajo previo.

Cuatro paneles, cuatro preguntas

El monitor de acciones muestra a la izquierda la lista cronológica de todo lo despachado; al seleccionar una entrada, el panel derecho ofrece varias vistas del mismo instante. Cada pestaña responde una pregunta distinta, y confundirlas es la causa más frecuente de mirar mucho y ver poco.

🏷️

Action

Qué pasó. El objeto despachado tal cual: su type y su payload. Es la intención declarada, antes de que ningún reducer la interprete.

🗂️

State

Cómo quedó el mundo. El árbol completo después de aplicar esa acción. Útil para verificar la forma, inútil para localizar el cambio.

🔍

Diff

Qué cambió exactamente. Solo las rutas que difieren entre el estado anterior y el nuevo. Es el panel que de verdad se usa a diario.

🧭

Trace

Quién lo pidió. La pila de llamadas del dispatch, que te lleva al componente o al thunk que originó la acción.

El panel Diff merece énfasis porque es el que convierte la inspección en diagnóstico. Ver el estado completo después de una acción rara vez enseña nada: en una app real son cientos de claves y el ojo no distingue lo que cambió de lo que siguió igual. El diff hace justo ese recorte y presenta una lista corta de rutas con su valor antiguo y su valor nuevo. La mayoría de los bugs de estado se manifiestan como una discrepancia entre lo que esperabas ver en ese diff y lo que hay: un campo que no debía tocarse, un array entero reemplazado donde bastaba un elemento, una bandera que quedó activa.

Trace es el más caro y el que menos gente activa, porque exige que el store capture la pila en cada dispatch. Cuando lo enciendes, cada acción lleva pegado el camino de código que la originó, y eso responde la pregunta que el resto de paneles no puede: no qué pasó, sino desde dónde. Es especialmente valioso con acciones despachadas indirectamente, donde el disparador está a varios saltos del componente visible.

Alrededor de esas cuatro vistas hay dos controles que multiplican su utilidad y que se pasan por alto con frecuencia. El primero es el filtro del monitor, que oculta por nombre las acciones que no te interesan y deja a la vista solo la conversación relevante; sin él, cualquier aplicación con actividad continua ahoga la señal. El segundo es el buscador sobre el árbol de estado, que localiza una clave concreta sin desplegar diez niveles a mano. Con ambos, la rutina de diagnóstico se vuelve mecánica: filtras hasta quedarte con la familia de acciones sospechosa, recorres sus diffs y buscas en el estado la clave que no cuadra.

El orden en que se consultan los paneles también importa más de lo que parece. La secuencia productiva es casi siempre Diff primero, para ver qué cambió; Action después, para comprobar si lo que cambió se corresponde con lo que la acción pedía; y Trace solo cuando la acción no debería haberse despachado en absoluto. Empezar por State, que es la tentación natural porque es el panel más grande, suele ser perder cinco minutos mirando cientos de claves correctas.

Por qué esto exige inmutabilidad

Aquí es donde la lección deja de ser sobre una extensión y pasa a ser sobre el modelo. El diff, el historial y todo lo que viene en las lecciones siguientes descansan sobre una condición: que el estado anterior siga existiendo cuando llega el nuevo. Un reducer que muta no la cumple. Si tu reducer escribe dentro del objeto que recibió, el estado anterior y el nuevo son la misma referencia, y comparar una cosa consigo misma no produce ninguna diferencia.

flowchart LR
subgraph inmutable [reducer puro]
  S0[estado 0] -->|accion A| S1[estado 1]
  S1 -->|accion B| S2[estado 2]
  S0 -.sigue vivo.-> D[diff calculable]
  S1 -.sigue vivo.-> D
end
subgraph mutable [reducer que muta]
  M0[estado unico] -->|accion A| M0
  M0 -->|accion B| M0
  M0 -.no hay pasado.-> X[diff imposible]
end
style D fill:#a6e3a1,color:#11111b
style X fill:#f38ba8,color:#11111b

El código lo muestra sin ambigüedad. Estas dos versiones producen la misma interfaz y son radicalmente distintas para las DevTools:

// Rompe la observabilidad: previo y siguiente son el mismo objeto.
function malReducer(state: Carrito, accion: Accion): Carrito {
  state.items.push(accion.payload)
  return state
}

// La preserva: el estado anterior queda intacto y comparable.
function bienReducer(state: Carrito, accion: Accion): Carrito {
  return { ...state, items: [...state.items, accion.payload] }
}

La segunda propiedad que el modelo exige es la serializabilidad. Para que una acción pueda mostrarse, exportarse y volver a aplicarse, tiene que ser un dato plano: objetos, arrays, números, cadenas, booleanos. En cuanto metes una promesa, una función, una instancia de clase o un elemento del DOM dentro de una acción o del estado, la representación deja de ser fiel y las capacidades de las lecciones siguientes empiezan a fallar. Redux Toolkit incluye por eso dos middleware de desarrollo, uno que detecta mutaciones y otro que detecta valores no serializables, y ambos son avisos de que estás gastando el capital sobre el que se sostiene toda la observabilidad.

El caso límite instructivo es el de un valor que casi es serializable. Una fecha nativa se convierte a texto al serializarse y no vuelve a ser una fecha al deserializar, de modo que sobrevive al monitor pero rompe la reproducción de una sesión importada. Un Map se serializa como un objeto vacío y desaparece sin avisar. La regla práctica que evita todos estos casos de una vez es guardar en el estado únicamente lo que sobreviviría intacto a una ida y vuelta por texto: números, cadenas, booleanos, arrays y objetos planos. Las fechas viven como marcas de tiempo numéricas y se formatean al leer; los mapas viven como objetos indexados por identificador, que además es la forma normalizada que el Nivel 5 ya recomendaba por otras razones.

⚠️
Immer no te libera del contrato, lo administra

Con createSlice escribes lo que parece una mutación y funciona. Es fácil concluir que la inmutabilidad ya no importa, y es exactamente la conclusión equivocada. Immer intercepta esas escrituras sobre un borrador y produce un objeto nuevo estructuralmente compartido: el contrato se sigue cumpliendo, solo que lo cumple la librería en tu lugar. La distinción se vuelve práctica en cuanto sales del borrador: si guardas una referencia al estado y la modificas después, o si mutas algo fuera del reducer, Immer no puede protegerte y las DevTools empiezan a mentir. La regla mental correcta no es ya puedo mutar, sino dentro del borrador y solo ahí puedo escribir como si mutara.

Lo que degrada la observabilidad sin romper la app

Hay un conjunto de prácticas que no producen errores visibles y sin embargo vacían de valor el historial. La primera es nombrar mal. Una acción llamada setState o update es sintácticamente válida y semánticamente inútil: el monitor se llena de entradas indistinguibles y el registro deja de contar una historia. El nombre debe declarar la intención del usuario o del sistema, no la operación técnica que provoca, porque el historial vale por lo que se puede leer en él sin abrir el código.

La prueba de calidad de un nombre es sencilla y se puede aplicar en diez segundos: si al leer la acción en el monitor no sabes qué quería conseguir quien la despachó, el nombre está mal, por muy correcto que sea el código que hay detrás.

La diferencia entre un historial legible y uno mudo cabe en una comparación de nombres:

// Mudo: describe la mecánica y no dice nada del dominio.
dispatch({ type: 'usuario/set', payload: { verificado: true } })

// Legible: declara la intención y el historial cuenta una historia.
dispatch({ type: 'usuario/correoVerificado', payload: { id } })

La segunda es el estado demasiado grueso. Guardar objetos gigantes o duplicar en el store datos que ya viven en la cache del servidor infla cada entrada del historial y hace ilegible el diff. Hay una variante del estado grueso que merece mención propia porque se cuela con las mejores intenciones: guardar en el store la respuesta completa de un extremo remoto en lugar de los campos que la interfaz usa. El coste inmediato es que cada diff se llena de rutas irrelevantes y que el historial pesa varias veces más de lo necesario; el coste diferido, que analizarás en la última lección de este nivel, es que en esa respuesta completa suelen viajar datos personales que nadie decidió conservar.

La tercera es el ruido de alta frecuencia: acciones despachadas en cada píxel de un arrastre o en cada tecla pulsada convierten el monitor en un torrente donde las acciones significativas se pierden. Para esos casos existen el filtrado y el agrupado, pero la solución real suele ser preguntarse si ese valor necesitaba pasar por el store.

📝
El historial es documentación viva del dominio

Un efecto lateral poco comentado: cuando las acciones están bien nombradas, el monitor se convierte en la mejor descripción existente de lo que tu aplicación sabe hacer. Alguien que nunca ha leído tu código puede usar la app durante cinco minutos y salir con la lista completa de operaciones del dominio, en orden y con sus datos. Ninguna documentación escrita a mano se mantiene tan actualizada, porque esta no se escribe: se deriva de lo que el sistema realmente hace. Trata los nombres de tus acciones con el mismo cuidado con el que tratarías los títulos de esa documentación, porque eso es exactamente lo que son.

Las DevTools no son una herramienta que añadiste: son una propiedad que te ganaste

El error de perspectiva más extendido sobre este nivel es leerlo como el capítulo de la extensión de navegador, como si las DevTools fueran un accesorio que se instala y que, con suerte, otra librería podría ofrecer también. La verdad es la contraria y es más severa: las DevTools no existen porque alguien las programara bien, existen porque el modelo de Redux hizo que fueran programables. Un depurador solo puede enseñarte lo que el programa se molestó en conservar, y casi todos los programas conservan únicamente el presente, porque mutar es sobrescribir y sobrescribir es destruir el pasado. Redux tomó la decisión costosa de no destruirlo —cada acción es un objeto que sobrevive, cada estado es una fotografía que nadie pisa— y de esa decisión, y solo de ella, se deducen el diff, el historial, el viaje en el tiempo y la reproducción de sesiones que verás en las lecciones siguientes. Por eso la pregunta correcta al evaluar cualquier gestor de estado no es si tiene DevTools, sino si su modelo de cambio permite que las tenga de verdad; y por eso Zustand puede engancharse a esta misma extensión solo en la medida en que respete el mismo contrato, ni un milímetro más allá. La lección profunda es que la observabilidad no se añade a un sistema, se diseña dentro de él: es la consecuencia visible de haber aceptado una restricción invisible. Cada vez que mutas fuera del borrador, cada vez que metes una promesa en el estado, cada vez que llamas update a una acción, no estás rompiendo una regla arbitraria del framework, estás gastando el capital que compraste con la ceremonia y quedándote con el coste sin el beneficio. La disciplina no era el precio de la herramienta: la disciplina era la herramienta, y la extensión solo es la ventana desde la que se mira.

⚔️ Audita el historial de tu propia aplicación
  1. Abre las DevTools sobre un módulo real y realiza una tarea completa de usuario, de principio a fin. No mires todavía: solo genera historia.
  2. Lee la lista de acciones sin abrir el código y escribe con ella la narración de lo que hizo el usuario. Cada punto donde no puedas hacerlo señala una acción mal nombrada.
  3. Selecciona las cinco acciones más significativas y compara sus paneles Diff con lo que esperabas que cambiara. Anota cada rama del estado que se tocó sin motivo.
  4. Activa Trace y localiza el origen de la acción cuya procedencia te resultó menos evidente. Comprueba si el camino real coincide con el que suponías.
  5. Introduce a propósito una mutación directa en un reducer, fuera del borrador de Immer, y observa cómo el diff deja de reflejar el cambio. Deshaz el experimento y verifica que el middleware de detección lo habría avisado.
  6. Cuenta cuántas de las acciones del historial son ruido de alta frecuencia. Para cada una, decide si ese valor tenía que pasar por el store o podía vivir fuera de él.