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

Parámetros de URL en Angular Router usando NgRx Router Store

Portada principal del artículo: Parámetros de URL en Angular Router usando NgRx Router Store

Cuando creamos aplicaciones con estado, el punto de entrada es clave para inicializar el estado de nuestros componentes. Sin embargo, muchas veces tenemos el requisito de conservar el estado de la aplicación dentro de la URL para que los usuarios puedan guardar en favoritos o compartir pantallas específicas, con el objetivo de mejorar la experiencia de usuario y facilitar la navegación.

En la mayoría de los casos, combinamos Angular Router y ActivatedRoute dentro de nuestros componentes para resolver estos escenarios, delegando toda esa responsabilidad a los componentes o, en otros casos, creando una mezcla entre componentes y effects para intentar solucionarlo.

Sigo de vacaciones en Menorca, así que aproveché esta mañana para practicar y aprender a manejar el estado en Angular Router, viendo cómo NgRx Router puede ayudar a mejorar mi código y quitarle responsabilidades a mis componentes.

El escenario

Quiero crear una página de edición donde los usuarios puedan modificar los detalles de un lugar seleccionado, compartir la URL y volver al mismo estado más adelante. Por ejemplo, http://localhost/places/2, donde 2 es el ID del lugar que se está editando. Además, los usuarios deben poder regresar a la página principal después de realizar una acción.

💡Este artículo forma parte de mi serie sobre NgRx. Si quieres seguir el paso a paso, te recomiendo revisarla:

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

Clona el repositorio start-with-ngrx. Este proyecto ya viene con NgRx y la aplicación lista. Luego, cámbiate a la rama crud-ngrx:

https://github.com/danywalls/start-with-ngrx.git
git checkout crud-ngrx

¡Es hora de programar!

La página de edición

Primero abre la terminal y, usando el Angular CLI, genera un nuevo componente:

ng g c pages/place-edit

Luego, abre app.routes.ts y registra el PlaceEditComponent con el parámetro /places/:id:

{
  path: 'places/:id',
  component: PlaceEditComponent,
},

Obtener el lugar a editar

Mi primera solución es una combinación entre el servicio, el effect, el router y ActivatedRoute. Esto va a requerir agregar lógica en varios lugares:

  • Agregar un método en el servicio de lugares.
  • Escuchar las acciones.
  • Configurar la acción de éxito para actualizar el estado del lugar seleccionado.
  • Leer el lugar seleccionado en PlaceEditComponent.

Primero, agrega el método getById en places.service.ts, el cual obtiene el lugar usando su id:

getById(id: string): Observable<Place> {
  return this.http.get<Place>(`${environment.menorcaPlacesAPI}/${id}`);
}

Luego, agrega nuevas acciones para manejar getById. Abre places.actions.ts y añade las acciones para editar, éxito y fallo:

// PlacePageActions
'Edit Place': props<{ id: string }>(),
 
// PlacesApiActions
'Get Place Success': props<{ place: Place }>(),
'Get Place Failure': props<{ message: string }>(),

Actualiza el reducer para manejar estas acciones:

on(PlacesApiActions.getPlaceSuccess, (state, { place }) => ({
  ...state,
  loading: false,
  placeSelected: place,
})),
on(PlacesApiActions.getPlaceFailure, (state, { message }) => ({
  ...state,
  loading: false,
  message,
})),

Abre place.effects.ts, agrega un nuevo effect para escuchar la acción editPlace, llama a placesService.getById y luego obtén la respuesta para despachar la acción getPlaceSuccess:

export const getPlaceEffect$ = createEffect(
  (actions$ = inject(Actions), placesService = inject(PlacesService)) => {
    return actions$.pipe(
      ofType(PlacesPageActions.editPlace),
      mergeMap(({ id }) =>
        placesService.getById(id).pipe(
          map((apiPlace) =>
            PlacesApiActions.getPlaceSuccess({ place: apiPlace })
          ),
          catchError((error) =>
            of(PlacesApiActions.getPlaceFailure({ message: error }))
          )
        )
      )
    );
  },
  { functional: true }
);

Esta solución parece prometedora. Ahora necesito despachar la acción editPlace e inyectar el router en place-card.component.ts para navegar a la ruta /places/:id:

goEditPlace(id: string) {
  this.store.dispatch(PlacesPageActions.editPlace({ id: this.place().id }));
  this.router.navigate(['/places', id]);
}

¡Funciona! Pero hay algunos efectos secundarios. Si seleccionas otro lugar y vuelves a la página, puede que la selección no se actualice y termines cargando el elemento anterior. Además, con conexiones lentas, podrías recibir un error de tipo "no encontrado" porque todavía se está cargando.

💡Una solución, gracias a Jörgen de Groot, es mover el router al effect. Abre el archivo places.effect.ts e inyecta el servicio y el router. Escucha la acción editPlace, obtén los datos, luego navega y despacha la acción.

El código final se ve así:

export const getPlaceEffect$ = createEffect(
  (
    actions$ = inject(Actions),
    placesService = inject(PlacesService),
    router = inject(Router)
  ) => {
    return actions$.pipe(
      ofType(PlacesPageActions.editPlace),
      mergeMap(({ id }) =>
        placesService.getById(id).pipe(
          tap(() => console.log('get by id')),
          map((apiPlace) => {
            router.navigate(['/places', apiPlace.id]);
            return PlacesApiActions.getPlaceSuccess({ place: apiPlace });
          }),
          catchError((error) =>
            of(PlacesApiActions.getPlaceFailure({ message: error }))
          )
        )
      )
    );
  },
  { functional: true }
);

Ahora resolvimos el problema de navegar únicamente cuando el usuario hace clic en la lista de lugares. Sin embargo, al recargar la página directamente esto no funciona, porque nuestro estado no está listo en la nueva ruta. Por suerte, tenemos la opción de usar los lifecycle hooks de los effects.

Los lifecycle hooks de los effects nos permiten disparar acciones cuando se registran los effects. Así puedo disparar la acción loadPlaces y tener el estado listo:

export const initPlacesState$ = createEffect(
  (actions$ = inject(Actions)) => {
    return actions$.pipe(
      ofType(ROOT_EFFECTS_INIT),
      map((action) => PlacesPageActions.loadPlaces())
    );
  },
  { functional: true }
);

Lee más sobre el ciclo de vida de los effects y ROOT_EFFECTS_INIT

Muy bien, ya tengo el estado listo, pero todavía tengo un problema al obtener el ID desde el estado de la URL.

Una solución rápida es leer activatedRoute en ngOnInit. Si el id está presente, despachamos la acción editPlace. Esto redirige y actualiza el estado de selectedPlace.

Por lo tanto, inyecta activatedRoute nuevamente en PlaceEditComponent e implementa la lógica en ngOnInit.

El código se ve así:

export class PlaceEditComponent implements OnInit {
  store = inject(Store);
  place$ = this.store.select(PlacesSelectors.selectPlaceSelected);
  activatedRoute = inject(ActivatedRoute);
 
  ngOnInit(): void {
    const id = this.activatedRoute.snapshot.params['id'];
    if (id) {
      this.store.dispatch(PlacesPageActions.editPlace({ id }));
    }
  }
}

¡Funciona! Finalmente, agregamos un botón de cancelar para redirigir a la ruta de lugares y vinculamos el evento click para llamar a un nuevo método cancel:

<button (click)="cancel()" class="button is-light" type="reset">Cancel</button>

Recuerda inyectar el router para llamar al método navigate hacia la URL de lugares. El código final se ve así:

export class PlaceEditComponent implements OnInit {
  store = inject(Store);
  place$ = this.store.select(PlacesSelectors.selectPlaceSelected);
  activatedRoute = inject(ActivatedRoute);
  router = inject(Router);
 
  ngOnInit(): void {
    const id = this.activatedRoute.snapshot.params['id'];
    if (id) {
      this.store.dispatch(PlacesPageActions.editPlace({ id }));
    }
  }
 
 cancel() {
  router.navigate(['/places']);
 }
}

Bien, funciona con todas las características, pero nuestro componente está manejando muchas tareas, como despachar acciones y redirigir la navegación. ¿Qué pasará cuando necesitemos más funcionalidades? Podemos simplificar todo usando NgRx Router, lo que reducirá la cantidad de código y las responsabilidades en nuestros componentes.

¿Por qué usar NgRx Router Store?

NgRx Router Store facilita conectar nuestro estado con los eventos del router y leer datos del router usando selectores integrados. Escuchar las acciones del router simplifica la interacción con los datos y los effects, manteniendo nuestros componentes libres de dependencias adicionales como el router o ActivatedRoute.

Acciones del Router

NgRx Router proporciona cinco acciones del router, las cuales se disparan en este orden:

  • ROUTER_REQUEST: cuando inicia una navegación.
  • ROUTER_NAVIGATION: antes de guards y resolvers, funciona durante la navegación.
  • ROUTER_NAVIGATED: cuando se completa la navegación.
  • ROUTER_CANCEL: cuando se cancela la navegación.
  • ROUTER_ERROR: cuando ocurre un error.

Lee más sobre ROUTER_ACTIONS

Selectores del Router

Nos ayuda a leer información del router, como parámetros de consulta (query params), datos, título y más, usando una lista de selectores integrados que ofrece la función getRouterSelectors.

export const { selectQueryParam, selectRouteParam} = getRouterSelectors()

Lee más sobre Router Selectors

Ahora que tenemos una visión general de NgRx Router, comencemos a implementarlo en el proyecto.

Configurar NgRx Router

Primero, necesitamos instalar NgRx Router. Este paquete proporciona selectores para leer datos del router y combinarlos con otros selectores para reducir código repetitivo en nuestros componentes.

En la terminal, instala ngrx/router-store usando los schematics:

ng add @ngrx/router-store

Luego, abre app.config y registra routerReducer y provideRouterStore:

  providers: [
    ...,
    provideStore({
      router: routerReducer,
      home: homeReducer,
      places: placesReducer,
    }),
    ...
    provideRouterStore(),
  ],

Ya tenemos NgRx Router en nuestro proyecto, ¡así que es hora de trabajar con él!

Lee más sobre cómo instalar NgRx Router

Simplificar usando NgRx RouterSelectors

En lugar de hacer una petición HTTP, voy a usar mi estado porque el effect init de NgRx siempre actualiza mi estado cuando el effect se registra. Esto significa que ya tengo los datos más recientes. Puedo combinar el selector selectPlaces con selectRouteParams para obtener selectPlaceById.

Abre el archivo places.selector.ts, crea y exporta un nuevo selector combinando selectPlaces y selectRouteParams.

El código final se ve así:

export const { selectRouteParams } = getRouterSelectors();
 
export const selectPlaceById = createSelector(
  selectPlaces,
  selectRouteParams,
  (places, { id }) => places.find((place) => place.id === id),
);
 
export default {
  placesSelector: selectPlaces,
  selectPlaceSelected: selectPlaceSelected,
  loadingSelector: selectLoading,
  errorSelector: selectError,
  selectPlaceById,
};

Perfecto, ahora es momento de actualizar y reducir todas las dependencias en PlaceEditComponent, y usar el nuevo selector PlacesSelectors.selectPlaceById. El código final se ve así:

export class PlaceEditComponent {
  store = inject(Store);
  place$ = this.store.select(PlacesSelectors.selectPlaceById);
}

Bien, ¿pero qué pasa con la acción de cancelar y redirigir? Podemos despachar una nueva acción, cancel, para manejar esto en el effect.

Primero, abre places.action.ts y agrega la acción 'Cancel Place': emptyProps(). El código final se ve así:

 export const PlacesPageActions = createActionGroup({
  source: 'Places',
  events: {
    'Load Places': emptyProps(),
    'Add Place': props<{ place: Place }>(),
    'Update Place': props<{ place: Place }>(),
    'Delete Place': props<{ id: string }>(),
    'Cancel Place': emptyProps(),
    'Select Place': props<{ place: Place }>(),
    'UnSelect Place': emptyProps(),
  },
});

Actualiza el método cancel en PlacesComponent y despacha la acción cancelPlace:

 cancel() { 
    this.#store.dispatch(PlacesPageActions.cancelPlace());
  }

El paso final es abrir place.effect.ts, agregar el effect returnHomeEffect$, inyectar el router y escuchar la acción cancelPlace. Usa router.navigate para redirigir cuando se despache la acción:

export const returnHomeEffect$ = createEffect(
  (actions$ = inject(Actions), router = inject(Router)) => {
    return actions$.pipe(
      ofType(PlacesPageActions.cancelPlace),
      tap(() => router.navigate(['/places'])),
    );
  },
  {
    dispatch: false,
    functional: true,
  },
);

Finalmente, el último paso es actualizar place-card para despachar la acción selectPlace y usar un routerLink:

        <a (click)="goEditPlace()" [routerLink]="['/places', place().id]" class="button is-info">Edit</a>

¡Listo! Lo logramos. Eliminamos las dependencias del router y de ActivatedRoute, mantuvimos el parámetro de la URL sincronizado y lo combinamos con los selectores del router.

Resumen

Aprendí cómo manejar el estado usando parámetros de la URL con NgRx Router Store en Angular. También integré NgRx con Angular Router para manejar el estado y la navegación, manteniendo limpios nuestros componentes. Este enfoque ayuda a gestionar mejor el estado y se combina con Router Selectors para leer fácilmente los datos del router.

Preguntas Frecuentes

¿Cómo leo parámetros de ruta de la URL en NgRx sin usar ActivatedRoute en los componentes?

El paquete @ngrx/router-store sincroniza el estado de Angular Router con la tienda de NgRx, lo que te permite crear selectores para leer parámetros de ruta directamente desde el store. Esto elimina la dependencia de ActivatedRoute en tus componentes y effects, hace que tu código sea más fácil de probar y mantiene el estado del router como una única fuente de verdad junto con el resto del estado de la aplicación.

¿Cómo instalo y configuro @ngrx/router-store en Angular?

Instala el paquete con npm install @ngrx/router-store y luego agrega provideRouterStore al arreglo de providers de la configuración de tu aplicación. De manera opcional, puedes proporcionar un serializador personalizado para controlar qué partes del snapshot del router se guardan. Una vez configurado, el estado del router estará disponible en el store bajo la clave router por defecto.

¿Cómo creo un selector para leer un parámetro ID de la ruta desde el store de NgRx?

Después de configurar @ngrx/router-store, utiliza el helper getRouterSelectors para obtener los selectores integrados, incluyendo selectRouteParam. Puedes crear tu propio selector combinando selectRouteParam con un nombre de parámetro específico como id. Luego, puedes usar este selector en componentes o effects como cualquier otro selector de NgRx.

¿Por qué es mejor almacenar los parámetros de la URL en el estado de NgRx que leerlos en cada componente?

Centralizar los parámetros de ruta en el store de NgRx reduce la duplicación de código y simplifica los componentes, ya que solo necesitan seleccionar el estado en lugar de inyectar y suscribirse a ActivatedRoute. También facilita la escritura de effects, porque pueden leer el ID de la ruta actual directamente desde el store sin tener que coordinar entre el router y las llamadas a servicios.

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.