Simplifica tu código de Angular con NgRx Entities: Por qué me encanta NgRx Entities

Durante el verano repasé mis habilidades con NgRx creando una pequeña aplicación para gestionar mis lugares favoritos. A lo largo de ese proceso disfruté mucho trabajando con NgRx, ya que tenía un control real sobre el estado de mi aplicación.
Algo que me generaba bastante ruido era la cantidad de selectores y acciones necesarios para definir operaciones CRUD. En un proyecto personal pequeño no representaba un gran problema, pero al construir una aplicación grande con múltiples slices, secciones, selectores y reducers, el código se volvía mucho más difícil de mantener.
Por ejemplo, escribir acciones para éxito, error, actualización y eliminación, junto con los selectores para cada una de estas operaciones, aumentaba la complejidad y requería más pruebas.
Aquí es donde entra NgRx Entities. NgRx Entities reduce el código repetitivo (boilerplate), simplifica las pruebas, acelera los tiempos de entrega y mantiene la base de código mucho más fácil de mantener. En este artículo te guiaré paso a paso para refactorizar la gestión de estado de los lugares en mi proyecto usando NgRx Entities para simplificar la lógica de las operaciones CRUD.
¿Qué es NgRx Entities y por qué usarlo?
Antes de escribir código, primero entendamos qué es NgRx Entities y por qué deberías considerar usarlo.
¿Qué es @ngrx/entity?
NgRx Entities es una extensión de NgRx que simplifica el trabajo con colecciones de datos. Proporciona un conjunto de utilidades que facilitan operaciones como agregar, actualizar y eliminar entidades del estado, además de seleccionar entidades desde el store.
¿Por qué conviene pasar a NgRx Entities?
Al crear operaciones CRUD para colecciones, escribir manualmente la lógica en el reducer y crear selectores repetitivos para cada operación resulta tedioso y propenso a errores. NgRx Entities se encarga de gran parte de este trabajo, reduciendo el volumen de código que necesitas escribir y mantener. Al minimizar el código repetitivo, NgRx Entities ayuda a disminuir la deuda técnica y simplifica la gestión del estado en aplicaciones de mayor escala.
¿Cómo funciona?
NgRx Entities ofrece herramientas como EntityState, EntityAdapter y selectores predefinidos para agilizar el trabajo con colecciones.
EntityState
La interfaz EntityState es el núcleo de NgRx Entities. Almacena la colección de entidades usando dos propiedades principales:
-
ids: un arreglo con los identificadores (IDs) de las entidades. -
entities: un diccionario donde cada entidad se almacena indexada por su ID.
interface EntityState<V> {
ids: string[] | number[];
entities: { [id: string | id: number]: V };
}Lee más sobre Entity State
EntityAdapter
El EntityAdapter se crea usando la función createEntityAdapter. Proporciona métodos de ayuda para gestionar entidades en el estado, tales como agregar, actualizar y eliminar elementos. Además, te permite configurar cómo se identifica cada entidad y cómo se ordenan los elementos.
export const adapter: EntityAdapter<Place> = createEntityAdapter<Place>();El EntityAdapter también te permite definir cómo se identifica cada entidad (selectId) y cómo se debe ordenar la colección mediante sortComparer.
Lee más sobre
EntityAdapter
Ahora que entendemos los conceptos básicos, veamos cómo podemos refactorizar la gestión de estado de los lugares en nuestra aplicación usando NgRx Entities.
Configuración del proyecto
Primero, clona el repositorio del artículo anterior y cámbiate a la rama que contiene la configuración básica del CRUD:
git clone https://github.com/danywalls/start-with-ngrx.git
git checkout crud-ngrx
cd start-with-ngrx💡 Este artículo es parte de mi serie para aprender NgRx. Si quieres seguir el paso a paso, échale un vistazo:
https://www.danywalls.com/understanding-when-and-why-to-implement-ngrx-in-angular
https://www.danywalls.com/how-to-debug-ngrx-using-redux-devtools
https://www.danywalls.com/how-to-implement-actioncreationgroup-in-ngrx
https://www.danywalls.com/how-to-use-ngrx-selectors-in-angular
https://danywalls.com/handling-router-url-parameters-using-ngrx-router-store
Esta rama contiene la configuración inicial donde NgRx ya está instalado y MockAPI.io está configurado para las llamadas a la API.
Nuestro objetivo es usar NgRx Entities para gestionar los lugares: refactorizar las acciones para las operaciones CRUD, actualizar el reducer para simplificarlo usando las operaciones del adaptador (como agregar, actualizar y eliminar lugares) y usar selectores para consultar la lista de lugares desde el store.
Instalación de NgRx Entities
Primero, instala las dependencias del proyecto con npm i, y luego agrega NgRx Entities usando schematics ejecutando ng add @ngrx/entity.
npm i
ng add @ngrx/entityPerfecto, ya tenemos todo listo para comenzar nuestra refactorización y actualizar la estructura del estado.
Refactorizando el estado
En la versión anterior del proyecto, definíamos manualmente arreglos y reducers para manejar el estado. Con NgRx Entities, dejamos que el adaptador gestione la lógica de la colección por nosotros.
Primero, abre places.state.ts y refactoriza PlacesState para extender de EntityState<Place>.
export type PlacesState = {
placeSelected: Place | undefined;
loading: boolean;
error: string | undefined;
} & EntityState<Place>;A continuación, inicializa el adaptador de entidad para nuestro modelo Place usando createEntityAdapter:
const adapter: EntityAdapter<Place> = createEntityAdapter<Place>();Por último, reemplaza el initialState manual por el que proporciona el adaptador a través de getInitialState:
export const placesInitialState = adapter.getInitialState({
error: '',
loading: false,
placeSelected: undefined,
});Hemos refactorizado el estado para usar EntityState e inicializamos el EntityAdapter para manejar la lista de lugares de forma automática.
Con el estado preparado, pasemos a actualizar las acciones para trabajar con NgRx Entities.
Refactorizando las acciones
En los artículos anteriores, manejaba manualmente las actualizaciones y modificaciones de las entidades. Ahora, utilizaremos NgRx Entities para gestionar actualizaciones parciales con Update<T>.
En places.actions.ts, actualizamos la acción Update Place para usar Update<Place>, lo que nos permite modificar únicamente las propiedades necesarias de una entidad:
import { Update } from '@ngrx/entity';
export const PlacesPageActions = createActionGroup({
source: 'Places',
events: {
'Load Places': emptyProps(),
'Add Place': props<{ place: Place }>(),
'Update Place': props<{ update: Update<Place> }>(), // Usar Update<Place>
'Delete Place': props<{ id: string }>(),
'Select Place': props<{ place: Place }>(),
'UnSelect Place': emptyProps(),
},
});De esta forma actualizamos las acciones para trabajar con NgRx Entities, usando el tipo Update para simplificar la gestión de cambios. Ahora veamos cómo impacta esto en el reducer y cómo podemos refactorizarlo usando los métodos del adaptador de entidades para agregar, actualizar y eliminar lugares.
Refactorizando el reducer
El reducer es donde NgRx Entities realmente destaca. En lugar de escribir lógica manual con copias inmutables para agregar, actualizar o eliminar lugares, ahora usamos los métodos que nos proporciona el adaptador de entidades.
Así es como podemos simplificar nuestro reducer:
import { createReducer, on } from '@ngrx/store';
import { adapter, placesInitialState } from './places.state';
import { PlacesApiActions, PlacesPageActions } from './places.actions';
export const placesReducer = createReducer(
placesInitialState,
on(PlacesPageActions.loadPlaces, (state) => ({
...state,
loading: true,
})),
on(PlacesApiActions.loadSuccess, (state, { places }) =>
adapter.setAll(places, { ...state, loading: false })
),
on(PlacesApiActions.loadFailure, (state, { message }) => ({
...state,
loading: false,
error: message,
})),
on(PlacesPageActions.addPlace, (state, { place }) =>
adapter.addOne(place, state)
),
on(PlacesPageActions.updatePlace, (state, { update }) =>
adapter.updateOne(update, state)
),
on(PlacesPageActions.deletePlace, (state, { id }) =>
adapter.removeOne(id, state)
)
);Utilizamos métodos del adaptador como addOne, updateOne, removeOne y setAll para manipular las entidades en el estado sin manipular arreglos a mano.
Otros métodos muy útiles que incluye el adaptador son:
-
addMany: Agrega múltiples entidades a la colección. -
removeMany: Elimina múltiples entidades según sus IDs. -
upsertOne: Agrega o actualiza una entidad dependiendo de si ya existe en el estado.
Consulta más sobre los métodos para reducers en
EntityAdapter.
Con el estado, las acciones y los reducers refactorizados, el siguiente paso es actualizar los selectores para aprovechar los que NgRx Entities nos ofrece por defecto.
Refactorizando los selectores
NgRx Entities nos brinda un conjunto de selectores predefinidos que facilitan consultar el store. Podemos usar selectores como selectAll, selectEntities y selectIds generados directamente por el adaptador.
Así es como refactorizamos los selectores en places.selectors.ts:
import { createFeatureSelector } from '@ngrx/store';
import { adapter, PlacesState } from './places.state';
const selectPlaceState = createFeatureSelector<PlacesState>('places');
const { selectAll, selectEntities, selectIds, selectTotal } = adapter.getSelectors(selectPlaceState);Estos selectores integrados reducen drásticamente la necesidad de crear funciones selectoras manuales para acceder a la lista o al diccionario de entidades.
Una vez que tenemos los selectores listos y reutilizables, es momento de actualizar los componentes de formulario para reflejar estos cambios y consumir el nuevo estado y las acciones.
Actualizando los componentes de formulario
Ahora que refactorizamos el estado, las acciones y los reducers, necesitamos sincronizar los componentes para adaptarlos a esta nueva estructura.
Por ejemplo, en PlaceFormComponent, actualizamos el método save para estructurar la carga útil con el formato esperado por Update<Place> al guardar cambios:
import { Component, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { FormsModule } from '@angular/forms';
import { AsyncPipe } from '@angular/common';
import { selectSelectedPlace } from '../../pages/places/state/places.selectors';
import { PlacesPageActions } from '../../pages/places/state/places.actions';
import { Place } from '../../entities/place.model';
@Component({
selector: 'app-place-form',
standalone: true,
imports: [FormsModule, AsyncPipe],
templateUrl: './place-form.component.html',
styleUrls: ['./place-form.component.scss'],
})
export class PlaceFormComponent {
store = inject(Store);
placeSelected$ = this.store.select(selectSelectedPlace);
delete(id: string) {
this.store.dispatch(PlacesPageActions.deletePlace({ id }));
}
save(place: Place, name: string) {
const update = { id: place.id, changes: { name } };
this.store.dispatch(PlacesPageActions.updatePlace({ update }));
}
}Con el componente de formulario actualizado para disparar las nuevas acciones, el último paso es revisar nuestros efectos para asegurarnos de que procesen correctamente las actualizaciones con NgRx Entities.
Refactorizando los efectos
Finalmente, ajustamos los efectos para trabajar con NgRx Entities. Lo único que necesitamos es actualizar la acción PlacesPageActions.updatePlace dentro de updatePlaceEffect$ para extraer el objeto update y pasar sus cambios al servicio:
export const updatePlaceEffect$ = createEffect(
(actions$ = inject(Actions), placesService = inject(PlacesService)) => {
return actions$.pipe(
ofType(PlacesPageActions.updatePlace),
concatMap(({ update }) =>
placesService.update(update.changes).pipe(
map((updatedPlace) =>
PlacesApiActions.updateSuccess({ place: updatedPlace })
),
catchError((error) =>
of(PlacesApiActions.updateFailure({ message: error })),
),
),
),
);
},
{ functional: true },
);¡Listo! Nuestra aplicación ya está funcionando con NgRx Entities y la migración fue muy directa. La documentación oficial de NgRx Entity resulta de gran ayuda en todo este proceso.
Conclusión
Tras migrar mi código a NgRx Entities, comprobé cómo ayudó a reducir la complejidad y el código repetitivo al trabajar con colecciones. NgRx Entities simplifica enormemente la gestión de colecciones y ofrece una gran variedad de métodos para la mayoría de los casos de uso habituales, eliminando la necesidad de escribir manualmente la lógica CRUD en reducers y selectores.
Espero que este artículo te motive a probar NgRx Entities la próxima vez que necesites gestionar colecciones en tus proyectos con NgRx.
Modern frontend development — Angular, React, TypeScript, accessibility, and performance.
Artículos Relacionados
Cómo usar NgRx Selectors en Angular
En NgRx, cuando queremos obtener datos del store, la forma más rápida suele ser usar store.select, pero tiene riesgos de tipado. Aprende a usar Selectors para consultar el estado con tipado seguro y mejor rendimiento.
Cómo manejar efectos secundarios en Angular usando NgRx Effects
Los efectos secundarios son habituales en nuestras aplicaciones. Aprende a manejarlos en Angular con NgRx Effects para evitar sobrecargar tus componentes y mantener tu estado limpio y predecible.
Cuándo y por qué usar REDUX NgRx en Angular
En un artículo anterior escribí sobre cómo manejar el estado en Angular usando servicios, pero cuando la aplicación crece y necesita gestionar un estado grande, se vuelve complejo de mantener. Te cuento cuándo y por qué conviene usar NgRx.
Cómo entender y usar la función inject() en Angular
Guía práctica sobre la función inject() de Angular: inicializadores de propiedades, eliminación de código repetitivo en herencia, guards funcionales e InjectionContext.
Cómo mejorar el rendimiento del build en Angular con ESBUILD
ESBuild es un compilador rápido de JavaScript utilizado por ViteJS. Puedes probarlo en Angular cambiando una sola línea en el archivo angular.json. Nota: está en modo experimental 😜 Cambia el builder de "@angular-devkit/build-angular:browser" a "@angula...
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.