Logo

dev-resources.site

for different kinds of informations.

Angular Router URL Parameters Using NgRx Router Store

Published at
8/8/2024
Categories
angular
ngrx
javascript
frontend
Author
danywalls
Categories
4 categories in total
angular
open
ngrx
open
javascript
open
frontend
open
Author
9 person written this
danywalls
open
Angular Router URL Parameters Using NgRx Router Store

When we build apps with state, the entry point is key to initialize our state for our components, but sometimes, we have requirements to preserve application state within the URL to allow users to bookmark or share specific application states, with the goal of improves user experience and make easy the navigation.

Most of case, we combine the Angular Router and ActivatedRoute in our components to solve these cases and delegate this responsibility to the components or in others cases making a mix between components and the effect to try solve it.

I'm continuing my holidays in Menorca, so I took this morning to learn and practice how to handle state in the Angular Router and how the ngrx router can help improve my code and reduce the responsibility in my components.

Scenario

I want to create an edit page where users can modify the details of a selected place, share the URL, and return to the same state later. For example, http://localhost/places/2, where 2 is the ID of the place being edited. Users should also be able to return to the home page after performing an action.

πŸ’‘This article is part of my series on learning NgRx. If you want to follow along, please check it out.

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

Clone the repo start-with-ngrx, this project bring with ngrx and the application ready and switch to the branch crud-ngrx

https://github.com/danywalls/start-with-ngrx.git
git checkout crud-ngrx
Enter fullscreen mode Exit fullscreen mode

It's time to coding!

The Edit Page

First open terminal and using the Angular CLI, generate a new component:

ng g c pages/place-edit
Enter fullscreen mode Exit fullscreen mode

Next, open app.routes.ts and register the PlaceEditComponent with the parameter /places/:id:

{
  path: 'places/:id',
  component: PlaceEditComponent,
},
Enter fullscreen mode Exit fullscreen mode

Get The Place To Edit

My first solution is a combination of the service, effect, router and activated route. It will require make add logic in several places.

  • Add method in the places service.

  • Listen actions

  • set the success to update the state of the selected place.

  • read the selected place in edit-place.component.

First, add getById method in the places.service.ts, it get the place by using the id.

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

Next, add new actions to handle the getById, open places.actions.ts add the actions to edit, success and failure:

// PlacePageActions
'Edit Place': props<{ id: string }>(),

// PlacesApiActions
'Get Place Success': props<{ place: Place }>(),
'Get Place Failure': props<{ message: string }>(),
Enter fullscreen mode Exit fullscreen mode

Update the reducer to handle these actions:

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

Open place.effects.ts, add a new effect to listen for the editPlace action, call placesService.getById, and then get the response to dispatch the getPlaceSuccess action.

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 }
);
Enter fullscreen mode Exit fullscreen mode

This solution seems promising. I need to dispatch the editPlace action and inject the router in place-card.component.ts to navigate to the /places:id route.

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

It works! But there are some side effects. If you select another place and go back to the page, the selection might not be updated, and you may load the previous one. Also, with slow connections, you might get a "not found" error because it is still loading.

πŸ’‘One solution, thanks to JΓΆrgen de Groot, is to move the router to the effect. Open the places.effect.ts file and inject the service and router. Listen for the editPlace action, get the data, then navigate and dispatch the action.

The final code looks like this:

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 }
);
Enter fullscreen mode Exit fullscreen mode

Now we fixed the issue of navigating only when the user click in the list of places, but when reloading the page that it's not working, because our state is not ready in the new route, but we have an option use the effect lifecycle hooks.

The effects lifecycle hooks allow us to trigger actions when the effects are register, so I wan trigger the action loadPlaces and have the state ready.

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

Read more about Effect lifecycle and ROOT_EFFECTS_INIT

Okay, I have the state ready, but I'm still having an issue when getting the ID from the URL state.

A quick fix is to read the activatedRoute in ngOnInit. If the id is present, dispatch the action editPlace. This will redirect and set the selectedPlace state.

So, inject activatedRoute again in the PlaceEditComponent and implement the logic in ngOnInit.

The code looks like this:

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 }));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

It works! Finally, we add a cancel button to redirect to the places route and bind the click event to call a new method, cancel.

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

Remember to inject the router to call the navigate method to the places URL. The final code looks like this:

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']);
 }
}
Enter fullscreen mode Exit fullscreen mode

Okay, it works with all features, but our component is handling many tasks, like dispatching actions and redirecting navigation. What will happen when we need more features? We can simplify everything by using NgRx Router, which will reduce the amount of code and responsibility in our components.

Why NgRx Router Store ?

The NgRx Router Store makes it easy to connect our state with router events and read data from the router using build'in selectors. Listening to router actions simplifies interaction with the data and effects, keeping our components free from extra dependencies like the router or activated route.

Router Actions

NgRx Router provide five router actions, these actions are trigger in order

  • ROUTER_REQUEST: when start a navigation.

  • ROUTER_NAVIGATION: before guards and revolver , it works during navigation.

  • ROUTER?NAVIGATED: When completed navigation.

  • ROUTER_CANCEL: when navigation is cancelled.

  • ROUTER_ERROR: when there is an error.

Read more about ROUTER_ACTIONS

Router Selectors

It helps read information from the router, such as query params, data, title, and more, using a list of built-in selectors provided by the function getRouterSelectors.

export const { selectQueryParam, selectRouteParam} = getRouterSelectors()
Enter fullscreen mode Exit fullscreen mode

Read more about Router Selectors

Because, we have an overview of NgRx Router, so let's start implementing it in the project.

Configure NgRx Router

First, we need to install NgRx Router. It provides selectors to read from the router and combine with other selectors to reduce boilerplate in our components.

In the terminal, install ngrx/router-store using the schematics:

ng add @ngrx/router-store
Enter fullscreen mode Exit fullscreen mode

Next, open app.config and register routerReducer and provideRouterStore.

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

We have the NgRx Router in our project, so now it's time to work with it!

Read more about install NgRx Router

Simplify using NgRx RouterSelectors

Instead of making an HTTP request, I will use my state because the ngrx init effect always updates my state when the effect is registered. This means I have the latest data. I can combine the selectPlaces selector with selectRouterParams to get the selectPlaceById.

Open the places.selector.ts file, create and export a new selector by combining selectPlaces and selectRouteParams.

The final code looks like this:

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,
};
Enter fullscreen mode Exit fullscreen mode

Perfect, now it's time to update and reduce all dependencies in the PlaceEditComponent, and use the new selector PlacesSelectors.selectPlaceById. The final code looks like this:

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

Okay, but what about the cancel action and redirect? We can dispatch a new action, cancel, to handle this in the effect.

First, open places.action.ts and add the action 'Cancel Place': emptyProps(). the final code looks like this:

 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(),
  },
});
Enter fullscreen mode Exit fullscreen mode

Update the cancel method in the PlacesComponent and dispatch the cancelPlace action.

 cancel() { 
    this.#store.dispatch(PlacesPageActions.cancelPlace());
  }
Enter fullscreen mode Exit fullscreen mode

The final step is to open place.effect.ts, add the returnHomeEffects effect, inject the router, and listen for the cancelPlace action. Use router.navigate to redirect when the action is dispatched.

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

Finally, the last step is to update the place-card to dispatch the selectPlace action and use a routerLink.

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

Done! We did it! We removed the router and activated route dependencies, kept the URL parameter in sync, and combined it with router selectors.

Recap

I learned how to manage state using URL parameters with NgRx Router Store in Angular. I also integrated NgRx with Angular Router to handle state and navigation, keeping our components clean. This approach helps manage state better and combines with Router Selectors to easily read router data.

ngrx Article's
30 articles in total
Favicon
πŸš€ Learning Through Experience: A Tale of NgRx Effects
Favicon
Announcing NgRx 19: NgRx Signals Features, Action Signals, and more!
Favicon
NGRX with Angular 16
Favicon
Angular Addicts #31: The new Resource API, effect updates & more
Favicon
Simplify Your Angular Code with NgRx Entities
Favicon
ngRx Store in Angular
Favicon
Angular Addicts #29: Angular 18.2, implicit libraries, the future is standalone & more
Favicon
NgRx Use Cases, Part III: Decision-making
Favicon
Angular Addicts #28: Angular 18.1 (w. the new @let syntax), Component testing, SSR guide & more
Favicon
Angular Router URL Parameters Using NgRx Router Store
Favicon
When to Use `concatMap`, `mergeMap`, `switchMap`, and `exhaustMap` Operators in Building a CRUD with NgRx
Favicon
How to Implement ActionCreationGroup in NgRx
Favicon
A single state for Loading/Success/Error in NgRx
Favicon
How to Debug NgRx Using REDUX DevTools in Angular
Favicon
When and Why to Use REDUX NgRx in Angular
Favicon
Announcing NgRx Signals v18: State Encapsulation, Private Store Members, Enhanced Entity Management, and more!
Favicon
Angular Addicts #27: NgRx 18, New RFC: DomRef API, Web storage with Signals & more
Favicon
Announcing NgRx 18: NgRx Signals is almost stable, ESLint v9 Support, New Logo and Redesign, Workshops, and more!
Favicon
Angular Addicts #26: Angular 18, best practices, recent conference recordings & more
Favicon
How to Handle Side Effects in Angular Using NgRx Effects
Favicon
How to Use NgRx Selectors in Angular
Favicon
Angular Addicts #25: Angular and Wiz will be merged, the differences between React and Angular & more
Favicon
Angular Addicts #24: Angular 17.3, Signals and unit testing best practices, Storybook 8 & more
Favicon
Creating a ToDo App with Angular, NestJS, and NgRx in a Nx Monorepo
Favicon
Here's how NgRx selectors actually work internally
Favicon
Angular Addicts #23: Angular 17.2, Nx 18, Signal forms, Analog, WebExpo & more
Favicon
All you need to know to get started with the NgRx Signal Store
Favicon
Angular Addicts #22: Angular 17.1, Signal Inputs, State management tips & more
Favicon
11 friends of state management in Angular
Favicon
The best of Angular: a collection of my favorite resources of 2023

Featured ones: