wandres.dev
MIGRAR DE REDUX · a Zustand o RTK

De Redux legacy a Redux Toolkit

Cuando el diagnóstico dice que el problema no era Redux sino su ceremonia manual, la ruta más barata no es cambiar de librería sino modernizar la que ya tienes. Esta lección recorre la migración de un Redux artesanal a Redux Toolkit en el orden que minimiza riesgo: primero configureStore, que es un cambio de una línea y aporta las garantías de inmediato; después los reductores a createSlice, uno cada vez y conviviendo con los antiguos; y por último los efectos. Incluye las trampas de la conversión mecánica —el reductor que mezcla mutación y retorno bajo Immer, las constantes de acción que otros slices escuchan— y el criterio para saber cuándo esta ruta es preferible a salir del ecosistema.

⏱ 17 min

Hay un escenario que las dos lecciones anteriores no cubren y que es, en la práctica, el más frecuente de todos: el equipo no odia Redux, odia el Redux que escribió en 2018. Constantes de acción declaradas a mano, creadores de acción que solo devuelven un objeto literal, reductores enormes con un switch que copia estructuras anidadas a base de propagación, un store cableado con applyMiddleware y una configuración de DevTools copiada de un blog. Ese código es doloroso, pero su dolor no proviene del modelo unidireccional, sino de que todo lo repetitivo del modelo se está escribiendo a mano. Cuando el diagnóstico es ese, la migración más rentable no cruza el ecosistema: se queda dentro y sustituye la ceremonia manual por la maquinaria que Redux Toolkit ya trae hecha. Es la ruta más barata que existe en este nivel, y también la más infravalorada, porque no tiene el atractivo narrativo de abandonar nada.

🎯 Al terminar esta lección sabrás
  • Reconocer el diagnóstico que hace preferible modernizar Redux antes que abandonarlo.
  • Ejecutar la migración en el orden de menor riesgo: configureStore, luego reductores, luego efectos.
  • Convertir un reductor artesanal a createSlice sin caer en las trampas de Immer.
  • Mantener la compatibilidad con las constantes de acción que otros slices escuchan.

Primero el store: un cambio de una línea

El orden de esta migración es contraintuitivo. Lo que más molesta son los reductores, y sin embargo no se tocan primero, porque el cambio de mayor beneficio por unidad de riesgo está en el punto de entrada. Sustituir createStore por configureStore es un cambio de unas pocas líneas que no altera ni un reductor ni una acción, y que a cambio activa de golpe las comprobaciones que llevan años faltándote.

// antes: cableado manual de middleware, devtools y comprobaciones inexistentes
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)))

// despues: mismo rootReducer, mismas acciones, garantias nuevas
const store = configureStore({ reducer: rootReducer })

Ese cambio no es cosmético. configureStore incorpora redux-thunk, conecta las DevTools y, en desarrollo, añade dos comprobaciones que actúan como auditoría automática: una detecta mutaciones accidentales del estado y otra, valores no serializables dentro del árbol. Es habitual que la primera ejecución tras el cambio revele mutaciones que llevaban años produciendo bugs de renderizado inexplicables, y que la segunda encuentre fechas o instancias de clase guardadas en el store. Ese hallazgo llega antes de haber reescrito una sola línea de lógica, y ya justifica el paso por sí mismo.

🛡️

Detección de mutaciones

Encuentra en desarrollo las escrituras directas sobre el estado que llevaban años pasando desapercibidas y produciendo renders perdidos.

📦

Serializabilidad

Delata fechas, mapas e instancias de clase dentro del árbol, que rompen la persistencia y el viaje en el tiempo.

🔌

Middleware por defecto

redux-thunk y las DevTools ya vienen conectadas, y la cadena se extiende con una función en vez de con composición manual.

🧾

Tipos que se infieren

El tipo del estado raíz y el del despachador se deducen del propio store, y dejan de mantenerse a mano en un archivo aparte.

Merece la pena resistir la prisa en este punto. La reacción natural al ver docenas de avisos de mutación es silenciarlos para poder continuar con la migración, y esa decisión desperdicia el hallazgo más valioso de toda la etapa. Cada aviso señala un lugar donde el estado se escribió sin pasar por un reductor, lo que significa que algún componente pudo no volver a dibujarse cuando debía, y esos son precisamente los bugs intermitentes que llevan años sin reproducirse a voluntad. Corregirlos antes de tocar ningún reductor separa además dos fuentes de riesgo que conviene no mezclar: los fallos que ya existían y los que introduzca la conversión.

Reductor a reductor, conviviendo con los antiguos

La segunda etapa es la que más código borra, y su propiedad decisiva es que createSlice y un reductor artesanal conviven sin ninguna capa de compatibilidad. combineReducers no distingue entre ambos: los dos son funciones que reciben estado y acción y devuelven estado. Puedes convertir un reductor por semana durante un año y el store seguirá funcionando cada día.

// artesanal: constante, creador, y un switch que copia a mano cada nivel
const AÑADIR = 'carrito/anadir'
const anadir = (id) => ({ type: AÑADIR, payload: id })

function carrito(estado = { lineas: [] }, accion) {
  switch (accion.type) {
    case AÑADIR:
      return { ...estado, lineas: [...estado.lineas, { id: accion.payload, n: 1 }] }
    default:
      return estado
  }
}

La versión equivalente elimina las tres piezas manuales de una vez: el tipo de la acción se deriva del nombre, el creador se genera y la copia estructural la resuelve Immer a partir de una escritura que parece directa pero no lo es.

const carritoSlice = createSlice({
  name: 'carrito',
  initialState: { lineas: [] as Linea[] },
  reducers: {
    anadir: (s, a: PayloadAction<string>) => { s.lineas.push({ id: a.payload, n: 1 }) },
  },
})

export const { anadir } = carritoSlice.actions
⚠️
Las dos trampas de la conversión mecánica

La primera es mezclar los dos estilos que Immer admite dentro del mismo reductor. Puedes mutar el borrador o devolver un estado nuevo, pero hacer ambas cosas en la misma función produce un comportamiento que no es el que ninguna de las dos lecturas sugiere; en particular, mutar y además devolver un valor distinto descarta la mutación de forma silenciosa. Elige un estilo por reductor y sé estricto. La segunda trampa es más traicionera: al convertir un slice, el tipo de sus acciones cambia de la constante antigua a la cadena generada, y cualquier otro reductor que escuchara esa constante en su switch deja de reaccionar sin que nada falle ni avise. Antes de convertir un slice, busca su constante en todo el repositorio; si alguien más la escucha, conserva el nombre exacto con la opción de preparación o migra ambos a la vez.

flowchart TD
A[redux artesanal] --> B[paso 1 configureStore]
B --> C[paso 2 reductores a createSlice]
C --> D[paso 3 efectos a thunks o listener]
B -.->|beneficio inmediato| G1[mutaciones detectadas]
C -.->|beneficio inmediato| G2[menos codigo repetido]
style B fill:#a6e3a1,color:#11111b
style C fill:#89b4fa,color:#11111b
style D fill:#f9e2af,color:#11111b

El orden de conversión dentro de esta etapa también admite criterio. Los candidatos ideales para empezar son los reductores con muchos casos y poca lógica —los que solo copian estructuras anidadas— porque su conversión es casi mecánica y el ahorro de líneas es máximo. Los que conviene dejar para el final son aquellos cuya lógica de negocio está enredada con la copia inmutable, donde separar qué se decide de cómo se escribe exige entender el dominio; convertir esos primero produce discusiones largas justo cuando el equipo todavía no confía en el proceso.

Los efectos al final, y el criterio de elección

Los efectos se dejan para el último lugar porque son la parte donde la conversión mecánica es imposible y hay que pensar. Un thunk artesanal que despacha tres acciones alrededor de una llamada de red se convierte en createAsyncThunk con una correspondencia razonable, pero una saga con cancelación, ventanas de tiempo o coordinación entre varias tareas no tiene traducción directa a listener middleware, y forzarla produce código peor que el original. La regla práctica es convertir los efectos triviales, dejar intactos los complejos mientras funcionen y reescribir solo aquellos que ya te estaban dando problemas por su cuenta.

Esa asimetría entre lo mecánico y lo pensado es también la razón por la que esta ruta es tan barata en conjunto. Las dos primeras etapas se pueden repartir entre cualquiera del equipo y revisarse casi de un vistazo, porque la corrección de cada cambio es local y comprobable; solo la tercera exige a alguien que entienda el flujo completo. Una migración de ecosistema, en cambio, convierte casi todo el trabajo en la categoría cara, porque cada pieza cambia de modelo a la vez que de sintaxis y ninguna revisión puede limitarse a comparar formas.

ℹ️
Cuándo esta ruta gana a salir del ecosistema

Tres señales apuntan a modernizar en vez de abandonar. La primera es el tamaño del equipo: cuantas más personas tocan el mismo estado, más valen las convenciones estrictas y el registro auditable de acciones, y menos pesa el ahorro de kilobytes. La segunda es la dependencia real de las DevTools: si tu equipo depura de verdad con el viaje en el tiempo y exporta sesiones para reproducir incidencias, ninguna alternativa ligera te devuelve esa capacidad. La tercera es la naturaleza del estado restante tras el paso uno de este nivel: si lo que queda sigue siendo un modelo grande con transiciones complejas, y no cuatro banderas, la estructura de Redux es inversión y no peaje. Cuando las tres señales están presentes, salir del ecosistema es pagar una migración cara para perder garantías que sí estabas usando.

Conviene fijar también un criterio de terminación para esta etapa, porque un Redux medio modernizado durante años es peor que uno artesanal coherente: el equipo mantiene dos estilos, y cada persona nueva aprende ambos sin saber cuál es el vigente. La regla que mejor funciona es prohibir el estilo antiguo para todo lo nuevo desde el primer día y convertir lo existente al ritmo que permita el trabajo de producto, con una fecha escrita para el último reductor.

Los tipos dejan de mantenerse a mano

Hay una cuarta etapa que rara vez se planifica y que suele ser la más agradecida por el equipo: dejar de escribir a mano los tipos del estado. En un Redux artesanal con TypeScript, alguien declaró en algún momento una interfaz del estado raíz y la ha ido actualizando cada vez que se añadía un slice. Esa interfaz miente casi siempre, porque olvidarla no rompe nada: el código compila, los selectores devuelven el tipo declarado y la divergencia con el árbol real solo se descubre cuando algo falla en tiempo de ejecución con un valor que el tipo juraba imposible.

// nada de esto se escribe: se deduce del store ya configurado
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch: () => AppDispatch = useDispatch

La diferencia entre declarar y deducir no es de comodidad sino de fiabilidad: un tipo deducido del store no puede quedarse desfasado, porque no existe un lugar donde olvidarse de actualizarlo. A partir de ahí, cada selector recibe el árbol correcto sin anotación, y el despachador conoce los thunks registrados, de modo que el error clásico de despachar una función sin el middleware que la entienda pasa a detectarse al compilar en vez de al ejecutar. Difundir los dos hooks tipados por el repositorio es un cambio mecánico que se puede hacer en paralelo a la conversión de reductores y que no depende de ella.

📝
Un efecto secundario útil de tipar bien

Al sustituir la interfaz escrita a mano por la deducida, es habitual que aparezcan errores de compilación en sitios que llevaban años funcionando. Casi ninguno es una regresión introducida por el cambio: son lugares donde el código leía una propiedad que el árbol real nunca tuvo, o donde el tipo declarado prometía que un valor no podía ser nulo y sí podía. Ese conjunto de errores es, en la práctica, un segundo informe de auditoría gratuito sobre el estado, y merece leerse con atención antes de silenciarlo a golpe de aserciones.

Modernizar es la migración que nadie presume y casi siempre la correcta

Existe un sesgo cultural en nuestra profesión que hace sistemáticamente invisible esta ruta, y conviene nombrarlo porque distorsiona decisiones caras. Cambiar de librería es una historia contable: tiene un antes y un después, se anuncia en una charla, se escribe en un artículo y produce la sensación nítida de haber avanzado. Modernizar la librería que ya tienes no produce ninguna de esas cosas; al terminar sigues usando Redux, no hay nada que anunciar, y el mérito de haber borrado dos mil líneas de ceremonia manual sin romper nada es indistinguible desde fuera de no haber hecho nada. Ese desequilibrio de reconocimiento empuja a equipos enteros hacia migraciones de ecosistema que no necesitaban, y el coste no se paga en el proyecto de migración sino en los dos años siguientes, cuando descubren que la herramienta nueva no traía el viaje en el tiempo que usaban a diario, o que las convenciones que consideraban burocracia eran lo único que impedía que veinte personas escribieran el mismo estado de veinte formas distintas. La disciplina que este nivel intenta enseñar es separar el diagnóstico del deseo: si el dolor viene de escribir a mano lo que una herramienta genera, la cura es dejar de escribirlo a mano, y esa cura cuesta semanas en vez de trimestres y no arriesga ninguna garantía que estuvieras usando. Si el dolor viene del modelo mismo —de que tu estado no encaja con un árbol único y transiciones explícitas— entonces sí hay que cruzar, y las lecciones anteriores explican cómo. Pero el orden correcto de las preguntas es ese, y no el inverso, porque una migración de ecosistema emprendida sobre un diagnóstico de ceremonia manual resuelve el síntoma por accidente y a un precio absurdo, mientras deja intacto todo lo que sí funcionaba.

⚔️ Moderniza tu Redux artesanal en tres etapas
  1. Sustituye createStore por configureStore sin tocar nada más y ejecuta la aplicación en desarrollo. Anota cada aviso de mutación y de valor no serializable que aparezca.
  2. Corrige esos avisos antes de seguir: son bugs reales que llevaban años ocultos y que ninguna etapa posterior arreglará por ti.
  3. Elige el reductor más repetitivo y conviértelo a createSlice. Comprueba que convive con los antiguos bajo combineReducers sin capa de compatibilidad.
  4. Antes de cada conversión, busca la constante de acción del slice en todo el repositorio y verifica si algún otro reductor la escucha.
  5. Convierte solo los efectos triviales a createAsyncThunk y deja intactas las sagas complejas que hoy funcionan. Anota cuáles reescribirías y por qué.
  6. Con la modernización a medias, evalúa las tres señales del criterio y escribe si tu destino final es quedarte en el ecosistema o salir de él.