Antes de migrar: auditar el store
Ninguna migración honesta empieza eligiendo librería: empieza con un inventario. Esta lección enseña a levantar el censo completo del store actual —una fila por porción de estado, sin agrupar— y a clasificar cada fila con cuatro preguntas de procedencia, lectores, escritores y caducidad. El resultado casi siempre sorprende: la mayor parte de lo que un store de Redux guarda no era estado de cliente sino una caché de servidor construida a mano, rodeada de derivados que no debían almacenarse y de estado muerto que nadie lee. Medir esa proporción antes de tocar nada es lo que convierte una migración en un proyecto dimensionado y no en una apuesta.
Migrar sin auditar es mudarse sin abrir las cajas: trasladas el desorden intacto a una casa nueva y descubres, meses después, que el problema nunca fue la casa. La tentación al abrir este nivel es preguntar a qué migramos, y esa pregunta es prematura, porque la elección de destino depende por completo de qué contiene realmente el origen. La auditoría que propone esta lección no es un trámite previo sino la pieza que decide todo lo demás: al terminarla sabrás qué proporción de tu store era estado del servidor disfrazado, cuánto era derivado que jamás debió persistirse, cuánto está muerto y cuánto es, de verdad, estado de cliente que merece un dueño nuevo. Ese reparto, y no una preferencia de librería, es lo que dimensiona la migración y a menudo lo que revela que la migración que imaginabas era otra.
- Levantar un inventario exhaustivo del
storecon una fila por porción de estado, sin agrupar porslice. - Clasificar cada fila con cuatro preguntas: procedencia, lectores, escritores y caducidad.
- Medir la proporción real de estado del servidor y usarla para dimensionar el trabajo.
- Detectar el estado muerto y el derivado almacenado antes de mover una sola línea.
El inventario: una fila por porción de estado
El error metodológico más frecuente al planificar una migración es auditar por slice. Un slice es una unidad de organización del código, no una unidad de naturaleza del estado, y dentro de uno cualquiera conviven habitualmente cuatro cosas distintas: la lista que vino de la red, la bandera de carga que acompaña a esa lista, el identificador seleccionado por el usuario y un total calculado a partir de todo lo anterior. Auditar el slice entero como una unidad te obliga a asignarle una única clase, y esa clase será falsa para tres de sus cuatro contenidos. La granularidad correcta de la auditoría es la hoja: cada clave terminal del árbol es una fila.
Escribir el inventario importa más de lo que parece, porque un inventario recordado siempre resulta más limpio que el real. La forma mínima de la tabla es la que fuerza a comprometerse con un juicio por fila:
// una fila por hoja del arbol; el slice no es la unidad de auditoria
type Procedencia = 'servidor' | 'cliente' | 'derivado' | 'muerto'
interface FilaDeAuditoria {
ruta: string // usuarios.porId, ui.panelAbierto, carrito.total
procedencia: Procedencia
lectores: number // cuantos componentes la leen hoy
escritores: string[] // que acciones la escriben
caduca: boolean // se vuelve obsoleta sin que nadie la toque
}
El árbol real se enumera, no se adivina, y el propio store sabe responder. Un recorrido de las claves en tiempo de ejecución da la lista completa de hojas y su tamaño aproximado, y sirve de esqueleto para la tabla:
const arbol = store.getState() as Record<string, unknown>
for (const [slice, contenido] of Object.entries(arbol)) {
const hojas = Object.keys(contenido as object)
const bytes = JSON.stringify(contenido).length
console.log(slice, hojas.length, bytes)
}
El tamaño en bytes no es una métrica ociosa: casi siempre delata la asimetría central de este nivel. Las hojas que ocupan el noventa y tantos por ciento del árbol son colecciones normalizadas de entidades traídas de la red, mientras que las banderas, los identificadores seleccionados y las preferencias de interfaz —lo que de verdad es estado de cliente— ocupan unas pocas decenas de bytes en total. Ver ese contraste en cifras es el primer momento incómodo y útil de la auditoría.
Conviene además acotar el ejercicio en el tiempo, porque una auditoría que aspira a la exhaustividad perfecta no termina nunca y acaba abandonada a medias, que es la peor de las salidas. Dos sesiones de un par de horas con dos personas bastan para un store de tamaño medio, y el rendimiento decreciente aparece pronto: las primeras veinte filas revelan el patrón, y las restantes lo confirman. Si al llegar a la mitad del árbol el reparto por procedencia se ha estabilizado, la muestra ya es suficiente para decidir, y refinarla más solo retrasa el trabajo que de verdad importa.
La procedencia de una hoja no siempre está en el código actual: a veces solo se entiende leyendo el cambio que la introdujo. Auditar con el historial a mano convierte preguntas irresolubles —por qué existe esta bandera, quién la puso— en respuestas de treinta segundos, y hacerlo en pareja evita el sesgo de quien escribió el slice y recuerda la intención en vez del resultado. Cuando ninguna de las dos personas sabe explicar para qué sirve una hoja y el historial tampoco lo aclara, esa hoja es candidata a estado muerto y merece una búsqueda específica antes que una clasificación de compromiso.
Las cuatro preguntas que clasifican
Clasificar por intuición produce la clasificación que uno desea; clasificar por preguntas produce la que hay. Las cuatro que siguen bastan para asignar procedencia a cualquier hoja, y su orden importa, porque la primera resuelve la mayoría de los casos y las otras tres refinan el resto.
De dónde vino
Si el valor entró por una respuesta de red, es estado del servidor, aunque lo hayas transformado, normalizado o mezclado con otros. La procedencia no se pierde al reformatear.
Quién lo lee
Un lector único delata estado local promovido a global sin motivo. No necesita migrar a otro store: necesita bajar a useState o a la URL.
Quién lo escribe
Si nadie lo escribe salvo la acción que lo trajo, es una copia. Si lo escriben muchas acciones sin relación entre sí, tienes dos conceptos fundidos en una hoja.
Si caduca solo
Un valor que se vuelve falso con el paso del tiempo sin que tu código lo toque es una caché. Necesita revalidación, y eso es exactamente lo que un store no sabe hacer.
La cuarta pregunta merece detenimiento porque es la que más gente responde mal. La obsolescencia no es un accidente que le ocurra a algunos datos: es la propiedad definitoria del estado del servidor. Un tema o una bandera de panel abierto no caducan nunca por sí solos, porque el cliente es su dueño y la verdad está donde vive el dato. Una lista de pedidos, en cambio, empieza a mentir en el instante siguiente a su llegada, y la única razón por la que tu aplicación parece funcionar es que el usuario recarga a menudo. Marcar caduca con honestidad en cada fila reordena la tabla entera.
La primera trampa es el dato de servidor editado en el cliente: un formulario que carga un perfil remoto, lo mete en el store y permite modificarlo. La tentación es clasificarlo como cliente porque el usuario escribe en él, pero son dos hojas superpuestas —el valor remoto, que caduca, y el borrador local, que no— y separarlas es parte de la auditoría, no una consecuencia posterior. La segunda trampa es el derivado almacenado: un total, un contador de elementos filtrados, una bandera de si la lista está vacía. Almacenar un derivado crea una segunda fuente de verdad que hay que mantener sincronizada a mano, y ninguna migración lo arregla, porque el defecto no está en la herramienta sino en el modelo. Esas hojas no migran a ningún sitio: desaparecen y renacen como selectores.
El veredicto y cómo se lee
Con la tabla completa, la auditoría produce un número: qué fracción de las hojas, y qué fracción de los bytes, cae en cada procedencia. Ese número es el plan de migración, porque cada procedencia tiene un destino distinto y un coste muy distinto.
flowchart TD ST[store de redux hoy] --> AUD[inventario hoja por hoja] AUD --> SRV[estado de servidor] AUD --> CLI[estado de cliente real] AUD --> DER[derivado almacenado] AUD --> MUE[estado muerto] SRV --> D1[cache de datos leccion 2] CLI --> D2[store ligero leccion 3] DER --> D3[selectores no migra] MUE --> D4[borrar no migra] style SRV fill:#f38ba8,color:#11111b style CLI fill:#a6e3a1,color:#11111b style DER fill:#89b4fa,color:#11111b style MUE fill:#45475a,color:#cdd6f4
Al leer el resultado conviene mirar las dos métricas por separado, porque cuentan historias distintas y ambas son útiles. El reparto por bytes mide dónde está el volumen y suele estar aplastantemente dominado por el servidor; el reparto por número de hojas mide dónde está la complejidad de mantenimiento, y ahí el estado de cliente pesa más de lo que su tamaño sugiere, porque cada bandera tiene sus acciones, sus lectores y sus reglas implícitas. Un store puede ser un noventa y cinco por ciento de servidor por bytes y solo un sesenta por ciento por hojas, y esa diferencia cambia el orden de las etapas.
El estado muerto merece una nota aparte porque es el hallazgo más barato de toda la auditoría y el que más gente omite. En cualquier store con años de vida hay hojas que ninguna vista lee: quedaron de una pantalla retirada, de una prueba que se quedó, de un rediseño a medias. Localizarlas es mecánico —una búsqueda del nombre de cada hoja en los selectores y en los componentes, y una lectura de las acciones que la escriben— y borrarlas reduce el alcance de la migración sin ningún riesgo de regresión, porque borrar algo que nadie lee no puede romper nada que alguien vea.
Es tentador estimar el esfuerzo por el número de líneas del store actual, y esa estimación siempre es pesimista por un factor grande. Las hojas de servidor no se reescriben: se borran y su función la asume una caché de datos con dos líneas por consulta, de modo que miles de líneas de reductores, acciones y thunks desaparecen sin sustituto equivalente. Las hojas derivadas tampoco se reescriben: se convierten en funciones puras que casi siempre ya existían dentro de algún selector. Lo único que se reescribe de verdad es el estado de cliente auténtico, y ese suele ser el resto más pequeño del inventario. Estimar por líneas de origen infla el proyecto y a menudo lo mata antes de empezar; estimar por filas de destino lo devuelve a su tamaño real.
Del inventario al plan de trabajo
Una tabla no es un plan. El paso que convierte una en otro es agrupar las filas por destino y ordenar los grupos por relación entre coste y beneficio, no por comodidad ni por el orden en que aparecen en el árbol. Esa agrupación se puede calcular sobre el propio inventario, y hacerlo por escrito impide la deriva habitual de empezar por el slice que más molesta sin comprobar si es también el más caro.
const destino = (f: FilaDeAuditoria): string => {
if (f.procedencia === 'muerto') return 'borrar ahora, riesgo cero'
if (f.procedencia === 'derivado') return 'convertir en selector, no migra'
if (f.procedencia === 'servidor') return 'cache de datos, leccion 2'
if (f.lectores <= 1) return 'bajar a useState o a la URL'
return 'estado de cliente real, leccion 3'
}
const plan = filas.map((f) => [f.ruta, destino(f)] as const)
La aritmética de ese reparto sorprende cada vez. Dos de los cinco destinos —borrar y convertir en selector— no son migración en ningún sentido: son limpieza que se puede desplegar la misma semana, sin dependencia de ninguna decisión de herramienta y sin riesgo apreciable. Un tercero, bajar a useState o a la URL, tampoco necesita store nuevo. Sumados, esos tres grupos suelen absorber una fracción sustancial de las filas, y ejecutarlos primero tiene una virtud que va más allá del ahorro: reduce el tamaño del problema antes de que nadie tenga que comprometerse con un destino.
El inventario no es un documento que se escribe una vez y se guarda. Conviene rehacerlo, en versión abreviada, después de cada etapa grande del nivel, porque los porcentajes cambian de forma drástica y el plan debería cambiar con ellos. Tras extraer el estado del servidor, el reparto que justificaba una migración de librería puede haberse convertido en un puñado de filas inocuas; y tras borrar el estado muerto y disolver los derivados, lo que quedaba de un slice temido a veces cabe en veinte líneas. Un plan calculado sobre el inventario inicial y ejecutado sin revisarlo arrastra durante meses una premisa que dejó de ser cierta en la primera semana.
Hay una asimetría profunda entre el trabajo de auditar y el de migrar que conviene entender antes de emprender ninguno de los dos. Migrar consiste en mover una estructura de un contenedor a otro conservando su forma, y por tanto conserva también sus defectos: un modelo de estado mal clasificado sigue mal clasificado en Zustand, y un derivado almacenado sigue desincronizándose en Jotai. Auditar, en cambio, consiste en preguntar por la naturaleza de cada dato, y esa pregunta tiene la propiedad rara de que sus respuestas eliminan trabajo en lugar de trasladarlo: la hoja muerta se borra, el derivado se disuelve en una función, la copia de servidor se evapora en cuanto una caché asume su papel. Por eso el resultado más común de una auditoría honesta no es un plan de migración más preciso, sino un plan mucho más pequeño —y en una minoría nada despreciable de casos, ninguno en absoluto, porque el store que parecía insoportable resultó ser una caché mal hecha que se sustituye en dos semanas dejando Redux intacto para las cuatro banderas que quedan. La madurez que este nivel exige empieza aquí: entender que la insatisfacción con una herramienta rara vez es información sobre la herramienta y casi siempre información sobre el modelo de datos que le hemos hecho sostener. Quien audita primero descubre qué parte de su dolor era estructural y qué parte era de librería, y solo entonces está en condiciones de decidir si merece la pena cambiar de librería. Quien migra primero traslada el dolor estructural íntegro, lo bautiza con un nombre nuevo y concluye, al cabo de un año, que la nueva herramienta también era mala.
- Enumera el árbol completo del
storeen tiempo de ejecución y anota cada hoja terminal con su tamaño aproximado en bytes. No agrupes porslice. - Responde las cuatro preguntas para cada fila: procedencia, número de lectores, lista de escritores y si el valor caduca sin que tu código lo toque.
- Suma los bytes por procedencia y calcula el porcentaje que representa el estado del servidor. Escribe el número antes de seguir leyendo el nivel.
- Marca como muerta toda hoja cuyo nombre no aparezca en ningún selector ni componente, y bórrala en un cambio aislado que no toque nada más.
- Localiza los derivados almacenados y anota, para cada uno, la función pura que lo reproduciría a partir de las hojas de las que depende.
- Cierra la auditoría con dos frases: qué fracción de tu
storeno era estado de cliente, y qué queda realmente por migrar una vez descontado todo lo anterior.