Skip to content

Entendiendo NGRX

Posted on:31 de octubre de 2024 at 23:00

¿Qué es NGRX y cómo funciona?

NGRX es una biblioteca basada en Redux, implementada para Angular, que se enfoca en gestionar el estado de la aplicación de manera reactiva. Su arquitectura se basa en conceptos clave como acciones, reducers, efectos, store y selectors. El flujo de trabajo en NGRX es cíclico y reactivo: al realizar una acción, el estado se modifica y se notifica a los componentes que dependen de él, lo que permite que estos se actualicen automáticamente sin necesidad de manejar el estado directamente en cada uno.

Para entenderlo mejor, veamos cómo funciona cada elemento de NGRX:

¿Cómo funciona el flujo de trabajo de NGRX?

Para visualizarlo mejor, aquí tienes una ilustración de cómo NGRX maneja el flujo de trabajo en Angular:

1. El usuario realiza una acción en la interfaz (por ejemplo, quiere agregar un producto al carrito de compras).

2. Esta acción desencadena una acción NGRX (por ejemplo, agregarProductoCarrito).

3. La acción llega al reducer, donde se actualiza el estado del carrito en el store.

4. Si es necesario, un effect intercepta esta acción, realiza una petición HTTP a una API y devuelve el resultado al store.

5. Los componentes, que escuchan el estado a través de selectors, reciben automáticamente los cambios y se actualizan en la UI.

Ejemplo

Ahora vamos a ver con un ejemplo muy sencillo como manejar el estado en lo que vendría a ser un carrito de compra.

1. Crear proyecto de Angular

ng new cart-ngrx
cd cart-ngrx

2. Instalar dependencias de NGRX

ng add @ngrx/store@latest
ng add @ngrx/effects@latest
ng add @ngrx/store-devtools@latest

3. Definir modelo de datos

Carpeta: src/app/models/product.model.ts

export interface Product {
  id: number;
  name: string;
  price: number;
}

export interface CartItem {
  product: Product;
  quantity: number;
}

4. Definir estado inicial

Carpeta: src/app/state/cart.state.ts

import { CartItem } from '../models/product.model';

export interface CartState {
  items: CartItem[];
}

export const initialCartState: CartState = {
  items: [
    {
      product: {
        id: 1,
        name: 'Product 1',
        price: 100
      },
      quantity: 1
    },
    {
      product: {
        id: 2,
        name: 'Product 2',
        price: 200
      },
      quantity: 2
    },
  ]
};

5. Definir acciones NGRX

Carpeta: src/app/state/cart.actions.ts

import { createAction, props } from '@ngrx/store';
import { Product } from '../models/product.model';

export const addProduct = createAction(
  '[Cart] Add Product',
  props<{ product: Product }>()
);

export const removeProduct = createAction(
  '[Cart] Remove Product',
  props<{ productId: number }>()
);

export const clearCart = createAction('[Cart] Clear Cart');

6. Crear reducer

Carpeta: src/app/state/cart.reducer.ts

import { createReducer, on } from '@ngrx/store';
import { addProduct, removeProduct, clearCart } from './cart.actions';
import { initialCartState } from './cart.state';

export const cartReducer = createReducer(
  initialCartState,
  on(addProduct, (state, { product }) => {
    const existingItem = state.items.find(item => item.product.id === product.id);
    const updatedItems = existingItem
      ? state.items.map(item =>
        item.product.id === product.id
          ? { ...item, quantity: item.quantity + 1 }
          : item
      )
      : [...state.items, { product, quantity: 1 }];

    return { ...state, items: updatedItems };
  }),
  on(removeProduct, (state, { productId }) => ({
    ...state,
    items: state.items.filter(item => item.product.id !== productId)
  })),
  on(clearCart, state => initialCartState),
  // on(clearCart, state => ({...state, items: []}))
);

7. Crear componente Cart

ng generate component cart

8. Agregar lógica al componente

import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { addProduct, removeProduct, clearCart } from '../state/cart.actions';
import { CartState } from '../state/cart.state';
import { Product, CartItem } from '../models/product.model';
import { AsyncPipe } from '@angular/common';

@Component({
  selector: 'app-cart',
  standalone: true,
  imports: [AsyncPipe],
  templateUrl: './cart.component.html',
  styleUrl: './cart.component.css'
})
export class CartComponent {
  cartItems$: Observable<CartItem[]>;
  constructor(private store: Store<{ cart: CartState }>) {
    this.cartItems$ = store.select(state => state.cart.items);
  }

  addProduct(product: Product) {
    this.store.dispatch(addProduct({ product }));
  }

  removeProduct(productId: number) {
    this.store.dispatch(removeProduct({ productId }));
  }

  clearCart() {
    this.store.dispatch(clearCart());
  }
}

9. Y por último el template

@if (cartItems$ | async; as cartItems) {
  @for (item of cartItems; track item.product.id) {
    <p>{{ item.product.name }} - {{ item.quantity }} - ${{ item.product.price * item.quantity }}</p>
    <button (click)="addProduct(item.product)">Añadir</button>
    <button (click)="removeProduct(item.product.id)">Eliminar</button>
  }
}

<br>
<button (click)="clearCart()">Vaciar Carrito</button>

¿Cuándo Usar NGRX y Cuándo No?

NGRX es útil pero no siempre es necesaria. Usarla depende de la complejidad de la aplicación y de cuántos componentes requieran datos centralizados.

Cuándo usar NGRX:

Cuándo evitar NGRX: