NgRx en Angular: el mismo patrón con RxJS
NgRx es Flux hablado en el idioma de Angular: el store es un observable, los selectores son streams memoizados y los efectos son middleware escrito con operadores de RxJS. Esta lección traduce término a término el vocabulario que ya dominas, muestra por qué la inyección de dependencias y los observables convierten la ceremonia de Redux en algo que el framework ya exigía de todos modos, examina los effects como la respuesta de Angular a la pregunta de dónde vive el efecto, y cierra evaluando qué gana y qué cuesta expresar el flujo unidireccional sobre un modelo de programación reactiva. El patrón no cambia; cambia el acento.
Si Redux es agnóstico, cabe esperar que otros ecosistemas lo hayan adoptado con sus propios materiales, y el caso más literal es NgRx en Angular. No es un gestor inspirado en Redux ni un primo lejano: es Redux, con acciones, reducers puros, un único árbol de estado y DevTools incluidos, reescrito sobre RxJS y sobre el inyector de dependencias de Angular. Quien llega a NgRx desde este track no tiene que aprender un modelo nuevo, sino un diccionario: donde decías useSelector ahora dices store.select, donde decías middleware ahora dices Effects, y donde tenías una suscripción manual ahora tienes un Observable al que el framework se suscribe por ti. La lección de hoy hace esa traducción palabra por palabra y examina qué cambia de verdad cuando el flujo unidireccional se expresa como un stream.
- Traducir el vocabulario de Redux al de NgRx sin perder ni una pieza del patrón.
- Entender el store de NgRx como un
Observabledel estado y qué implica esa forma. - Leer un
Effectde RxJS como lo que es: middleware para lo que ocurre entre dos acciones. - Valorar por qué la ceremonia que pesaba en React resulta natural en Angular.
El mismo patrón, otro diccionario
La estructura de NgRx es reconocible línea a línea para quien viene de Redux Toolkit. Hay acciones con tipo y carga, hay reducers puros que reciben estado y acción y devuelven estado nuevo, hay un store único e inmutable, hay selectores memoizados equivalentes a los de Reselect y hay una extensión de navegador que muestra el historial. Incluso la variante moderna, createFeature, reduce el boilerplate con la misma estrategia que createSlice: agrupar acciones, reducer y selectores en una declaración cohesionada.
createAction
Equivale al action creator de RTK. Devuelve una función tipada que produce acciones con un tipo textual único y su carga.
createReducer
Reducer puro con manejadores por acción. Usa Immer por debajo, igual que createSlice, para escribir mutaciones aparentes.
createSelector
El mismo Reselect del Nivel 5, memoizado y componible, pero devolviendo un stream en lugar de un valor puntual.
createEffect
La capa de efectos: escucha el flujo de acciones, hace el trabajo impuro y emite acciones nuevas. Middleware con forma de stream.
Conviene subrayar que esta correspondencia no es una coincidencia ni una inspiración vaga: NgRx se presentó explícitamente como Redux para Angular y mantuvo la compatibilidad con la extensión de Redux DevTools precisamente para heredar su instrumental. Cuando leas su documentación reconocerás los tres principios del Nivel 2 enunciados casi con las mismas palabras, y cuando depures verás el mismo historial de acciones con el mismo viaje en el tiempo. Ese parentesco declarado es lo que convierte a NgRx en el mejor primer puerto del nivel: no hay que buscar el patrón bajo el disfraz, porque el disfraz es transparente.
La diferencia estructural con Redux es que en NgRx todo se registra en el inyector de Angular en lugar de componerse a mano en un archivo de configuración. El store, los efectos y las funcionalidades se declaran como proveedores, y los componentes los reciben por inyección de dependencias. Esto suena a detalle administrativo, pero tiene una consecuencia real: la disciplina que en React había que imponer con convenciones de carpetas —dónde vive el reducer, quién compone el store, quién registra el middleware— en Angular la impone el propio sistema de módulos y proveedores.
El store es un stream
La traducción más importante no es de nombres sino de forma. En Redux, el store es un objeto al que preguntas por su estado; en NgRx, el store es un Observable que emite el estado cada vez que cambia. Seleccionar no devuelve un valor, devuelve otro Observable derivado. Y la vista no lee: se suscribe.
import { Component, inject } from '@angular/core'
import { Store } from '@ngrx/store'
import { seleccionarPendientes } from './tareas.selectors'
import { tareasActions } from './tareas.actions'
@Component({
selector: 'app-tareas',
template: `
<p>Pendientes: {{ pendientes() }}</p>
<button (click)="completar()">Completar una</button>
`,
})
export class TareasComponent {
private store = inject(Store)
// selectSignal convierte el stream en una senal sincrona y legible en plantilla
pendientes = this.store.selectSignal(seleccionarPendientes)
completar() {
this.store.dispatch(tareasActions.completarPrimera())
}
}
Ese fragmento contiene la lección entera de la lección anterior en versión Angular: el store se obtiene por inyección en lugar de por contexto, el selector memoizado hace el mismo trabajo que useSelector, y dispatch es idéntico. Lo único genuinamente distinto es que la capa de enlace no la escribes tú ni la aporta un paquete de integración: la aporta el sistema de reactividad del framework, que sabe suscribirse a un Observable con la tubería async o convertirlo en una señal.
La llegada de las señales a Angular provocó la misma confusión que provocan las signals en cualquier ecosistema: se leyó como el fin de los gestores de estado. No lo es, y distinguirlo es importante. Una señal resuelve el problema de la capa de enlace —cómo se entera la vista de que un valor cambió, con grano fino y sin suscripciones manuales— exactamente el mismo problema que resolvía useSyncExternalStore en la lección anterior. Pero una señal no te dice quién puede escribir el estado, ni deja rastro de por qué cambió, ni te da un historial auditable. Esas son las garantías del patrón, no del mecanismo de notificación, y por eso NgRx expone selectSignal en lugar de desaparecer: cambió el cable, no la arquitectura.
Effects: dónde vive el efecto en Angular
En el Nivel 4 dedicamos una lección entera a la pregunta de dónde vive el efecto, y NgRx responde con la solución más elegante que ha dado el ecosistema: un efecto es una función que transforma el stream de acciones en otro stream de acciones. No es un thunk que despacha manualmente, ni una saga con generadores; es una tubería declarativa que dice qué acciones escucha, qué trabajo impuro hace y qué acción emite al terminar.
cargarTareas = createEffect(() =>
this.acciones.pipe(
ofType(tareasActions.cargar),
switchMap(() =>
this.api.traerTareas().pipe(
map((tareas) => tareasActions.cargaExitosa({ tareas })),
catchError((error) => of(tareasActions.cargaFallida({ error: error.message }))),
),
),
),
)
Hay una propiedad de este diseño que merece nombre propio porque no la tiene ninguna de las alternativas que vimos en el Nivel 4: el efecto no conoce a quien lo disparó ni a quien consumirá su resultado. Solo declara qué tipo de acción escucha y qué tipos emite, de modo que la funcionalidad entera se acopla al flujo de acciones y no a componentes concretos. Eso permite añadir un segundo efecto que reaccione a la misma acción —registrar analítica, invalidar una caché, mostrar una notificación— sin tocar ni una línea del primero, y es la razón por la que en aplicaciones grandes la capa de efectos crece sin volverse un nudo.
Léelo despacio y reconocerás el ciclo de createAsyncThunk del Nivel 3 escrito en otra notación: una acción de inicio, una petición, una acción de éxito y una de fallo. Lo que RxJS añade es control explícito sobre la concurrencia mediante el operador que elijas: switchMap cancela la petición anterior si llega otra acción, concatMap las encola, exhaustMap ignora las nuevas mientras una está en curso, mergeMap las lanza todas en paralelo. Esa elección, que en un thunk queda implícita y suele ser fuente de bugs sutiles con respuestas fuera de orden, aquí es una decisión visible en una sola palabra.
switchMap
Cancela lo anterior. La elección correcta para búsquedas mientras se teclea o para recargas donde solo importa el último resultado.
concatMap
Encola y respeta el orden. Para escrituras donde cada operación depende del estado que dejó la anterior.
exhaustMap
Ignora lo nuevo mientras trabaja. El antídoto del doble clic en un botón de envío o de inicio de sesión.
mergeMap
Todo en paralelo, sin garantía de orden. Legítimo para operaciones independientes y peligroso para todo lo demás.
Ese cuadro es, en la práctica, la mayor aportación de NgRx al patrón. En Redux con thunks la concurrencia se gestiona a mano —una bandera de carga, una comprobación de si la respuesta sigue siendo relevante, a veces un contador de peticiones— y ese código artesanal es donde se esconden los bugs difíciles de reproducir. Aquí la política de concurrencia es una decisión explícita, revisable en una revisión de código y cambiable en un carácter, lo que convierte una clase entera de errores en una elección de diseño visible.
flowchart LR C[componente] -->|dispatch| A[stream de acciones] A --> R[reducers puros] R --> S[estado unico] S --> SEL[selectores memoizados] SEL -->|observable o signal| C A --> E[effects con rxjs] E -->|accion de exito o fallo| A style A fill:#f9e2af,color:#11111b style R fill:#a6e3a1,color:#11111b style E fill:#f38ba8,color:#11111b
Por qué la ceremonia pesa menos aquí
Angular recorrió además el mismo camino que veremos en Vue con Pinia y en React con Zustand: junto al store clásico existe hoy NgRx SignalStore, una forma más ligera que declara estado, cálculos derivados y métodos en una sola pieza, sin acciones ni reducers explícitos, apoyándose en las señales para la notificación. Que el ecosistema con más ceremonia del mundo haya producido también su versión reducida confirma que la tendencia no es una moda de una comunidad concreta, sino la misma reevaluación en todas partes: cuando el entorno gana capacidad de observar cambios con grano fino, parte de la declaración explícita deja de ser necesaria.
El reproche estándar a NgRx es el mismo que hundió a Redux en React: demasiado boilerplate para lo que hace. Pero el reproche se evalúa siempre en relación con el coste base del ecosistema, y ahí Angular parte de otro sitio. Un desarrollador de Angular ya escribe módulos, ya declara proveedores, ya usa decoradores, ya compone tuberías de RxJS en los servicios y ya vive con tipado estricto. La ceremonia de NgRx no es un impuesto nuevo, sino más de lo que ya pagaba, y por eso su curva de adopción es mucho más suave que la de Redux para alguien que llegaba de un React minimalista.
Si NgRx duele en un proyecto, casi nunca es por las líneas de código que exige, sino porque el equipo no domina RxJS. Un efecto con mergeMap donde tocaba switchMap produce respuestas fuera de orden que sobrescriben datos frescos con datos viejos; un stream sin takeUntil deja suscripciones vivas que filtran memoria; un catchError colocado fuera de la tubería interna mata el efecto entero tras el primer fallo, dejando la funcionalidad muerta hasta recargar la página. Ninguno de esos tres bugs es un bug de Flux ni de NgRx: son bugs de programación reactiva. La conclusión práctica es que adoptar NgRx sin invertir en RxJS es adoptar la mitad de la herramienta, y que el equipo que sí lo domina obtiene a cambio un control de concurrencia que ni thunks ni sagas le dan con tanta claridad.
Lo verdaderamente instructivo de NgRx no es que Angular tenga su Redux, sino lo que revela sobre la relación entre un patrón y su entorno: el patrón permanece idéntico —acciones como único vector de cambio, reducers puros, un árbol de verdad, efectos aislados en la frontera— mientras que su expresión adopta íntegramente los materiales del ecosistema que lo hospeda, hasta el punto de resultar irreconocible a primera vista para quien solo lo aprendió en una encarnación. En React el flujo se expresó con hooks y contexto porque eso es lo que React ofrece; en Angular se expresa con observables, inyección de dependencias y decoradores porque eso es lo que Angular ofrece; y el resultado es que dos ingenieros que dominan exactamente la misma arquitectura pueden mirarse el código sin reconocerse, discutiendo sobre useSelector frente a store.select como si fuesen ideas distintas cuando son la misma idea con dos acentos. De ahí se sigue una consecuencia práctica que vale para el resto de tu carrera: la pregunta útil al aterrizar en un stack desconocido no es qué librería de estado usa, porque la respuesta es un nombre que no te enseña nada, sino cuáles son sus cuatro piezas —quién puede escribir, qué función pura decide el resultado, dónde se aísla lo impuro y cómo se entera la vista— porque esas cuatro preguntas tienen respuesta en cualquier ecosistema y su conjunto es toda la arquitectura. Quien las hace reconoce el patrón bajo cualquier disfraz y es productivo en días; quien busca el nombre de la librería aprende un vocabulario que caduca con la próxima versión mayor y vuelve a empezar de cero cada vez que cambia de trabajo.
- Toma un slice de
Redux Toolkitque ya tengas escrito y lista sus piezas: acciones, reducer, selectores y thunks. - Escribe la equivalencia de cada pieza en NgRx sin implementarla todavía. Comprueba que no sobra ni falta ninguna.
- Convierte un
createAsyncThunken uncreateEffecty decide de forma explícita qué operador de concurrencia le corresponde, justificando por qué. - Prueba a cambiar ese operador por
mergeMapy provoca a propósito una respuesta fuera de orden con dos peticiones de latencia distinta. - Expón un selector memoizado con
selectSignaly con la tuberíaasyncen dos componentes, y anota qué cambia en la plantilla y qué no cambia en la arquitectura. - Escribe en cinco líneas qué parte de tu diseño sería idéntica en React y qué parte depende de RxJS. Esa frontera es el resumen de la lección.