Fundamentos de las Features en NgRx

Features — Beneficios en utilizar createFeature function

Rodrigo Bosarreyes
6 min readJan 23, 2024

Hay tres componentes principales para la gestión del estado global con @ngrx/store: acciones, reductores y selectores. Para un estado de característica particular, creamos un reductor para manejar las transiciones de estado basadas en las acciones despachadas y selectores para obtener segmentos del estado de la feature. Además, necesitamos definir un nombre de feature necesario para registrar el reductor de la feature en el store de NgRx. Por lo tanto, podemos considerar la feature de NgRx como una agrupación del nombre de la feature, el reductor de la feature y los selectores del estado de la feature en particular.

Podemos considerar una feature de NgRx como una agrupación del nombre de la feature, el reductor y los selectores del estado de una feature en particular.

Más claro, creo que no se puede decir, tal como dice la documentación oficial, las feature se utilizan para agrupar el reductor y los selectores de una parte del estado global, es simplemente un encapsulador de estas características.

Alguno de los beneficios de utilizar features en una aplicación son:

  • ✅ Modularidad: Al dividir el estado de la aplicación en features, cada parte del estado se maneja de forma independiente. Esto facilita la organización del código y su mantenimiento, ya que cada feature es un módulo con su propio conjunto de acciones, reducers y selectores.
  • ✅ Mejor Organización del Estado: Las features ayudan a organizar el estado de manera lógica y coherente. Esto es especialmente útil en aplicaciones grandes, donde el estado puede volverse complejo y difícil de manejar.
  • ✅ Desacoplamiento: Las features promueven un diseño de software desacoplado. Cada parte del estado es independiente de las otras, lo que reduce las dependencias cruzadas y mejora la calidad del código.

¿Cómo se crean features?

Para crear una feature en ngrx podemos utilizar la función createFeature. La función createFeature define un objeto que representa una característica del estado, incluyendo su nombre (que lo identifica dentro del estado global), su reducer y sus selectores. Este objeto luego se utiliza para registrar la característica en el estado global de la aplicación, generalmente a través de la función StoreModule.forFeature o en nuestro caso que utilizamos Standalone Components, a través de la función provideState, en el módulo de Angular donde se define la feature.

Este método recibe hasta tres parámetros:

  • name: Un nombre único que identifica la feature dentro del estado global. Este nombre se utiliza cuando se registra la feature en el store.
  • reducer: El reducer asociado con esta feature del estado. Este reducer manejará las acciones relacionadas con esta parte del estado.
  • extraSelectors: Los selectores asociados con esta feature del estado. Los selectores son funciones que permiten seleccionar y derivar datos del estado. Este parámetro es opcional.

Un objeto creado con la función createFeature contiene el nombre de la feature, su reducer, un selector de la feature y un selector para cada propiedad del estado de la feature. Todos los selectores generados tienen el prefijo "select", y el selector de la feature tiene el sufijo "State".

Por ejemplo, si tenemos el siguiente estado:

interface State {
products: Product[];
loading: boolean;
selectedProduct: number | null;
}

const initialState: State = {
books: [],
loading: false,
selectedProduct: null
};

Podríamos crear la siguiente feature:

export const productsFeature = createFeature({
name: 'products',
reducer: createReducer(
initialState,
on(ProductListPageActions.search, (state, action) => ({
...state,
loading: false
})),
),
extraSelectors: ({ selectProductsState, selectSelectedProduct }) => ({
selectFilteredProduct: createSelector(
selectProductsState,
selectSelectedProduct,
(products, selectedProduct) => products.filter(product => product.id === selectedProduct),
),
}),
});

El cual nos genera el siguiente objeto:

const {
name, // nombre de la feature
reducer, // reducer de la feature
selectProductsState, // selector de la feature
selectProducts, // selector de la propiedad `products`
selectLoading, // selector de la propiedad `loading`
selectFilteredProduct, // select personalizado
} = productsFeature;

¿Cómo se registra una feature?

Podemos registrar nuestra feature o bien desde el inicio de la aplicación o bien utilizando “lazy loading”:

Si lo queremos registrar desde el inicio, invocamos la función provideState(feature) en nuestros providers de la clase de configuración de la aplicación (app.config.ts):

import { provideStore, provideState } from '@ngrx/store';
import { productsFeature } from './products.reducer';

export const appConfig: ApplicationConfig = {
providers: [
provideStore(),
provideState(productsFeature)
],
}

Si lo queremos registrar de manera diferida, debemos hacer lo mismo, pero en nuestro archivo de rutas:

import { Route } from '@angular/router';
import { provideState } from '@ngrx/store';

import { productsFeature } from './products.reducer';

export const routes: Route[] = [
{
path: 'products',
providers: [
provideState(productsFeature)
]
}
];

De esta manera registramos la feature con su reductor y sus selectors, pero en caso de utilizar effects debemos invocar la función provideEffects en el archivo de rutas de la feature:

import { Route } from '@angular/router';
import { provideState } from '@ngrx/store';

import { productsFeature } from './products.reducer';
import * as productsEffects from './effects/products.effects';

export const routes: Route[] = [
{
path: 'products',
providers: [
provideState(productsFeature),
provideEffects(productsEffects)
]
}
];
Alto allí

Este es artículo es parte de mi curso de Introducción a NgRx, donde abordamos temas como Store, Actions, Reducers, Selectors y Effects. Si no te suena algún tema este artículo, ¡te recomiendo revisar mi curso!

Implementando Features

Vamos a recordar nuestra maravillosa aplicación 🎉Ng Party Planner🎉. Nuestra aplicación ya cuenta con un Store, varias Actions, Reducers, Selectors e incluso Effects, Todos estos pertenecen al “módulo” de Guests, sin embargo, se encuentran registrados en la store global, por lo que podríamos (y deberíamos) agruparlos todos en una feature. Vamos a refactorizar nuestro código para cumplir este objetivo.

Vamos a empezar con el archivo guest.reducer.ts, la variable guestsReducer ya no la exportaremos, pero la reutilizaremos para construir la feature, para ello simplemente debemos “const”:

export guestsReducer = createReducer(
initialState,
//...
);

Ahora, creamos la variable guestsFeature con ayuda de la función createFeature, además, definimos también los selectores que tenemos en guests.selectors.ts:

export const guestsFeature = createFeature({
name: 'guests',
reducer: guestsReducer,
extraSelectors: ({ selectGuestsState }) => ({
selectConfirmedGuests: createSelector(selectGuestsState, (guest) =>
guest.filter((g) => g.isAttendeeConfirmed === true)
),
selectRejectedGuests: createSelector(selectGuestsState, (guest) =>
guest.filter((g) => g.isAttendeeConfirmed === false)
),
selectUnknownGuests: createSelector(selectGuestsState, (guest) =>
guest.filter((g) => g.isAttendeeConfirmed === null)
),
}),
});

Para facilitar la refactorización en otros componentes, vamos a exportar el objeto deconstruido generado:

export const {
name,
reducer,
selectGuestsState,
selectConfirmedGuests,
selectRejectedGuests,
selectUnknownGuests,
} = guestsFeature;

Nos movemos al archivo app.config.ts, dejaremos de registrar el estado guests en nuestra store, además de sus efectos:

export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideAnimations(),
provideStore(),
provideHttpClient(),
MessageService,
],
};

Y en lugar de registrarlos allí, los registramos en app.router.ts:

import { Routes } from '@angular/router';
import { provideState } from '@ngrx/store';
import { guestsFeature } from './core/store/guests/guest.reducer';
import { provideEffects } from '@ngrx/effects';
import * as guestsEffects from './core/store/guests/guest.effects';

export const routes: Routes = [
// otras rutas...
{
path: 'guests',
loadComponent: () =>
import('./features/guests/components/guests/guests.component').then(
(m) => m.GuestsComponent
),
providers: [provideState(guestsFeature), provideEffects(guestsEffects)],
},
];

Por último, en el archivo guests-dataview.component.ts modificamos el import para que en lugar de utilizar los selectores del archivo guests.selectors.ts utilicemos los que acabamos de implementar:

// otros imports...
import {
selectConfirmedGuests,
selectGuestsState,
selectRejectedGuests,
selectUnknownGuests,
} from '../../../../core/store/guests/guest.reducer';

(también es necesario actualizar el nombre de algunos selectores en este componente, porque los selectores antiguos tenían uno diferente)

Y ahora sí, podemos eliminar el archivo guests.selectors.ts y disfrutar de un código un poco más legible y organizado.

--

--