web-dev-qa-db-fra.com

Comment attacher ngrx/store avec une protection de routeur angulaire

Je suis un débutant avec ngrx/store et ceci est mon premier projet l’utilisant.

J'ai correctement configuré mon projet angulaire avec ngrx/store et je peux envoyer une action de chargement après l'initialisation de mon composant principal, comme ceci:

ngOnInit() { this.store.dispatch({type: LOAD_STATISTICS}); }

J'ai configuré un effet pour charger les données lorsque cette action est envoyée:

@Effect()
loadStatistik = this.actions.ofType(LOAD_STATISTICS).switchMap(() => {
    return this.myService.loadStatistics().map(response => {
        return {type: NEW_STATISTICS, payload: response};
    });
});

Mon réducteur ressemble à ceci:

reduce(oldstate: Statistics = initialStatistics, action: Action): Statistik {
    switch (action.type) {
        case LOAD_STATISTICS:
            return oldstate;
        case NEW_STATISTICS:
            const newstate = new Statistics(action.payload);

            return newstate;
    ....

Bien que cela fonctionne, je ne sais pas trop comment l'utiliser avec un routeur de protection, car je n'ai actuellement besoin d'envoyer une seule fois LOAD_ACTION.

De plus, le fait qu'un composant doive envoyer une action LOAD pour charger les données initiales ne me semble pas correct. Je m'attendrais à ce que le magasin lui-même sache qu'il doit charger des données et je n'ai pas à envoyer d'action au préalable. Si tel était le cas, je pourrais supprimer la méthode ngOnInit () de mon composant.

J'ai déjà examiné l'application ngrx-example-app mais je n'ai pas vraiment compris comment cela fonctionne.

MODIFIER:

Après avoir ajouté un garde de résolution qui renvoie l'observable ngrx-store, l'itinéraire n'est pas activé. Voici la résolution:

   @Injectable()
  export class StatisticsResolver implements Resolve<Statistik> {
    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Statistik> {
        // return Observable.of(new Statistik());
        return this.store.select("statistik").map(statistik => {
        console.log("stats", statistik);

        return statistik;
    });
}

C'est la route:

const routes: Routes = [
    {path: '', component: TelefonanlageComponent, resolve: {statistik:  TelefonieStatistikResolver}},
];
9
Marco Rinck

Je viens de le résoudre moi-même après une contribution précieuse de AngularFrance. Comme je suis toujours débutant, je ne sais pas si c'est comme cela que cela est supposé être fait, mais ça marche.

J'ai implémenté un CanActivate Guard comme ceci:

@Injectable()
export class TelefonieStatistikGuard implements CanActivate {
    constructor(private store: Store<AppState>) {}

    waitForDataToLoad(): Observable<Statistik> {
        return this.store.select(state => state.statistik)
            .filter(statistik => statistik && !statistik.empty);
    }

    canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
        this.store.dispatch({type: LOAD_STATISTIK});

        return this.waitForDataToLoad()
            .switchMap(() => {
                return Observable.of(true);
            });
        }
    }
}

La méthode canActivate (...) envoie d'abord une action pour charger les données. Dans waitForDataToLoad (), nous filtrons le fait que les données sont déjà présentes et non vides (détail d'implémentation de ma logique métier).

Une fois que cela retourne true, nous appelons switchMap pour renvoyer un observable de true.

6
Marco Rinck

Je ne comprends pas très bien pourquoi vous enverriez les données résolues à votre composant via le résolveur. L’intérêt de la création d’un magasin ngrx est d’avoir une seule source de données. Donc, ce que vous voulez simplement faire ici, c'est vous assurer que les données sont vraies. Au repos, le composant peut extraire les données du magasin à l’aide d’un sélecteur.

Je peux voir que vous appelez LOAD_ACTION à partir de la méthode ngOnInit de votre composant. Vous ne pouvez pas appeler une action pour résoudre des données, ce qui conduit ensuite au composant, sur Init dont vous appelez l'action! C'est pourquoi votre routeur ne charge pas la route.

Je ne sais pas si vous avez compris cela, mais c'est logique!

En termes simples, voici ce que vous faites. Vous fermez une pièce à clé, puis enfoncez la clé dans l’espace sous la porte et vous vous demandez pourquoi la porte ne s’ouvre pas.

Gardez la clé avec vous!

La méthode de résolution du gardien devrait appeler le LOAD_ACTION. La résolution doit ensuite attendre le chargement des données, puis le routeur doit accéder au composant.

Comment tu fais ça?

Les autres réponses sont la configuration des abonnements, mais le fait est que vous ne voulez pas faire cela dans vos gardes. Ce n'est pas une bonne pratique de s'abonner aux gardes, car ils devront alors être désabonnés, mais si les données ne sont pas résolues ou si les gardes renvoient false, le routeur ne se désabonnera jamais et nous aurons un fouillis.

Donc, utilisez take(n). Cet opérateur prendra les valeurs n de l'abonnement, puis le supprimera automatiquement.

De plus, dans vos actions, vous aurez besoin de LOAD_STATISTICS_SUCCESS et d'un LOAD_STATISTICS_FAIL. Comme votre méthode de service peut échouer!

Dans le réducteur State, vous auriez besoin d'une propriété loaded, qui devient true lorsque l'appel de service aboutit et que l'action LOAD_STATISTICS_SUCCESS est appelée.

Ajoutez un sélecteur dans le réducteur principal, getStatisticsLoaded, puis dans votre gaurd, votre configuration ressemblerait à ceci:

resolve(): Observable<boolean> {

this.store.dispatch({type: LOAD_STATISTICS});

return this.store.select(getStatisticsLoaded)
    .filter(loaded => loaded)
    .take(1);

}

Ainsi, ce n'est que lorsque la propriété chargée devient true que le filtre permettra au flux de continuer. take prendra la première valeur et le transmettra. La résolution est alors terminée et le routeur peut continuer.

Encore une fois, take va tuer l'abonnement.

7
notANerdDev

Je suppose que par "garde de routeur" vous voulez dire une garde resolve? Comme dans le cas où vous souhaiteriez que les données soient chargées avant activant la route?

Si oui, alors tout devrait bien se jouer:

  • Les valeurs de ngrx/store sont exposées en tant qu'observables (par exemple, dans leur documentation, store.select('counter') contient un observable).
  • Dans Angular resolve gardes peut renvoyer des observables .

Ainsi, vous pouvez simplement renvoyer le ngrx/store observable à partir de votre résolution:

class MyResolver implements Resolve<any> {

  constructor(private store: Store<any>) {}

  resolve(): Observable<any> {
    // Adapt code to your situation...
    return this.store.select('statistics');
  }
}

Cela étant dit, votre configuration semble un peu complexe. J'ai tendance à considérer l'état comme données transitoires (état condensé d'un menu, données devant être partagées entre plusieurs écrans/composants pendant la durée de la session, par exemple l'utilisateur actuel), mais je ne le suis pas. bien sûr, je chargerais une grande tranche de données du backend dans l’état.

Que gagnez-vous en stockant les statistiques dans l'état? (simplement en les chargeant et en les affichant dans un composant)

4
AngularChef

Si le chargement échoue, vous voudrez probablement annuler la navigation. Cela peut être fait en jetant une erreur. La chose la plus importante est - le retour de l'observable doit être complété ou terminé avec une erreur.

resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
): Observable<boolean> {

    this.store.dispatch(new LoadStatistic(route.params.someParam));

    return this.store.pipe(select(selectStatisticModel),
        filter(s => !s.isLoading),
        take(1),
        map(s => {
            if (!s.isLoadSuccess) {
                throw s.error;
            }
            return true;
        })
    );
}
2
Mārcis

Cette réponse pour les personnes frustrées d’utiliser ngrx/effect dans cette situation. Peut-être vaut-il mieux utiliser une approche simple sans ngrx/effets.

canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
    // this will only dispatch actions to maybe clean up, NO EFFECTS 
    this.store.dispatch({type: LOAD_STATISTIK});     

  return this.service.loadOne()
     .do((data) => {
         this.store.dispatch({type: LoadSuccess, payload: data })
      }).map(() => {
      return true; 
    })  
}
0
alexKhymenko

Cela considère également une demande ayant échoué:

canActivate(): Observable<boolean> {
    this.store.dispatch({type: LOAD_STATISTICS);

    return this.store.select((state) => state.statistics)
      .filter((statistics) => statistics.loaded || statistics.loadingFailed)
      .map((statistics) => statistics.loaded);
  }
0
Martin Cremer