Lorsque nous construisons des applications avec un état, le point d'entrée est essentiel pour initialiser notre état pour nos composants, mais parfois, nous avons des exigences pour préserver l'état de l'application dans l'URL afin de permettre aux utilisateurs de mettre en signet ou de partager des états d'application spécifiques, avec le objectif d'améliorer l'expérience utilisateur et de faciliter la navigation.
Dans la plupart des cas, nous combinons Angular Router et ActivatedRoute dans nos composants pour résoudre ces cas et déléguons cette responsabilité aux composants ou dans d'autres cas, nous faisons un mélange entre les composants et l'effet pour essayer de le résoudre.
Je continue mes vacances à Minorque, j'ai donc pris ce matin pour apprendre et pratiquer comment gérer l'état dans le routeur angulaire et comment le routeur ngrx peut m'aider à améliorer mon code et à réduire la responsabilité dans mes composants.
Je souhaite créer une page d'édition où les utilisateurs peuvent modifier les détails d'un lieu sélectionné, partager l'URL et revenir au même état plus tard. Par exemple, http://localhost/places/2, où 2 est l'ID du lieu en cours de modification. Les utilisateurs devraient également pouvoir revenir à la page d'accueil après avoir effectué une action.
?Cet article fait partie de ma série sur l'apprentissage de NgRx. Si vous souhaitez suivre, n'hésitez pas à y jeter un œil.
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
Clonez le repo start-with-ngrx, ce projet apporte avec ngrx et l'application prête et passez à la branche crud-ngrx
https://github.com/danywalls/start-with-ngrx.git git checkout crud-ngrx
Il est temps de coder !
Ouvrez d'abord le terminal et à l'aide de la CLI Angular, générez un nouveau composant :
ng g c pages/place-edit
Ensuite, ouvrez app.routes.ts et enregistrez le PlaceEditComponent avec le paramètre /places/:id:
{ path: 'places/:id', component: PlaceEditComponent, },
Ma première solution est une combinaison du service, de l'effet, du routeur et de l'itinéraire activé. Il faudra ajouter de la logique à plusieurs endroits.
Ajouter une méthode dans le service des lieux.
Écoutez les actions
définissez le succès pour mettre à jour l'état du lieu sélectionné.
lisez le lieu sélectionné dans edit-place.component.
Tout d'abord, ajoutez la méthode getById dans places.service.ts, elle obtient le lieu en utilisant l'identifiant.
getById(id: string): Observable{ return this.http.get (`${environment.menorcaPlacesAPI}/${id}`); }
Ensuite, ajoutez de nouvelles actions pour gérer le getById, ouvrez places.actions.ts et ajoutez les actions à modifier, succès et échec :
// PlacePageActions 'Edit Place': props<{ id: string }>(), // PlacesApiActions 'Get Place Success': props<{ place: Place }>(), 'Get Place Failure': props<{ message: string }>(),
Mettez à jour le réducteur pour gérer ces actions :
on(PlacesApiActions.getPlaceSuccess, (state, { place }) => ({ ...state, loading: false, placeSelected: place, })), on(PlacesApiActions.getPlaceFailure, (state, { message }) => ({ ...state, loading: false, message, })),
Ouvrez place.effects.ts, ajoutez un nouvel effet pour écouter l'action editPlace, appelez placesService.getById, puis obtenez la réponse pour envoyer l'action 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 } );
Cette solution semble prometteuse. Je dois envoyer l'action editPlace et injecter le routeur dans place-card.component.ts pour accéder à la route /places:id.
goEditPlace(id: string) { this.store.dispatch(PlacesPageActions.editPlace({ id: this.place().id })); this.router.navigate(['/places', id]); }
Ça marche ! Mais il y a quelques effets secondaires. Si vous sélectionnez un autre lieu et revenez à la page, la sélection risque de ne pas être mise à jour et vous risquez de charger la précédente. De plus, avec des connexions lentes, vous pourriez obtenir une erreur « introuvable » car le chargement est toujours en cours.
?Une solution, grâce à Jörgen de Groot, consiste à déplacer le routeur vers l'effet. Ouvrez le fichier places.effect.ts et injectez le service et le routeur. Écoutez l'action editPlace, récupérez les données, puis naviguez et envoyez l'action.
Le code final ressemble à ceci :
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 } );
Maintenant, nous avons résolu le problème de la navigation uniquement lorsque l'utilisateur clique dans la liste des lieux, mais lors du rechargement de la page, cela ne fonctionne pas, car notre état n'est pas prêt dans le nouvel itinéraire, mais nous avons la possibilité d'utiliser l'effet hooks de cycle de vie .
Les hooks de cycle de vie des effets nous permettent de déclencher des actions lorsque les effets sont enregistrés, je veux donc déclencher l'action loadPlaces et avoir l'état prêt.
export const initPlacesState$ = createEffect( (actions$ = inject(Actions)) => { return actions$.pipe( ofType(ROOT_EFFECTS_INIT), map((action) => PlacesPageActions.loadPlaces()) ); }, { functional: true } );
En savoir plus sur le cycle de vie des effets et ROOT_EFFECTS_INIT
D'accord, j'ai l'état prêt, mais j'ai toujours un problème lors de l'obtention de l'ID à partir de l'état de l'URL.
Une solution rapide consiste à lire l'activateRoute dans ngOnInit. Si l'identifiant est présent, distribuez l'action editPlace. Cela redirigera et définira l'état selectedPlace.
Alors, injectez à nouveau activateRoute dans le PlaceEditComponent et implémentez la logique dans ngOnInit.
Le code ressemble à ceci :
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 })); } } }
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.
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']); } }
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.
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.
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
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()
Read more about Router Selectors
Because, we have an overview of NgRx Router, so let's start implementing it in the project.
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
Next, open app.config and register routerReducer and provideRouterStore.
providers: [ ..., provideStore({ router: routerReducer, home: homeReducer, places: placesReducer, }), ... provideRouterStore(), ],
We have the NgRx Router in our project, so now it's time to work with it!
Read more about install NgRx Router
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, };
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); }
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(), }, });
Update the cancel method in the PlacesComponent and dispatch the cancelPlace action.
cancel() { this.#store.dispatch(PlacesPageActions.cancelPlace()); }
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, }, );
Finally, the last step is to update the place-card to dispatch the selectPlace action and use a routerLink.
Edit
Done! We did it! We removed the router and activated route dependencies, kept the URL parameter in sync, and combined it with router selectors.
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.
Source Code: https://github.com/danywalls/start-with-ngrx/tree/router-store
Resources: https://ngrx.io/guide/router-store
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!