🇺🇸 EN
Frontend
·9 min read·↗·🇺🇸 EN

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

Portada principal del artículo: 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/when-to-use-concatmap-mergemap-switchmap-and-exhaustmap-operators-in-building-a-crud-with-ngrx

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/entity

Perfecto, 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.

Parte de la serie Frontend

Modern frontend development — Angular, React, TypeScript, accessibility, and performance.

Ver serie completa

Artículos Relacionados

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.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.