דלגו לתוכן

NGRX Actions

פורסם בתאריך 23 בנובמבר 2023

על מנת ללמוד איך לעבוד עם api’s זה נכון לכתוב את הקוד בעצמך, אז בואו נלמד איך ליצור actions באמצעות NGRX.
בשיעור זה יש 2 תרגילים לתלמיד, מתחת לכל תרגיל תמצאו את הפתרון לתרגיל זה.

מהן actions?

  • Actions נשלחים ל store באמצעות store.dispatch(someAction)
  • ה store ישלח את ה actions ל reducers שישנם ב store שלנו, וכאשר הם מתבצעים ישנה שינוי ב state.

המשמעות של action היא להגיד שמשהו קרה באפליקציה, רוב הפעמים זה משהו שהמשתמש עשה כמו ללחוץ על כפתור או להפעיל אירוע מסוים.
לעתים ה action ישלח גם מידע נוסף, לדוגמא אם יש לנו action שאנחנו רוצים לשלוח אחרי תגובה מהשרת, נשלח לעיתים תגובת השרת כ payload ל action.
Action מכיל מזהה ונתונים אופציונליים { type: string, additionalData?: ..., moreData?: ... }.

createAction

ישנם 2 פונקציות יצירת actions ב @ngrx/store createAction ו createActionGroup.
נתחיל עם createAction שמשמשת ליצירת action יחיד.

import { createAction, props } from '@ngrx/store';
export const setRepos = createAction(
'[GroupName] Identifier For Action',
props<{ additionalData: any, anotherData: any, asManyDataAsYouWant: any, anyNameHereYouWant: any }>()
);

שימו לב שה action שלנו מכיל מזהה ייחודי, נהוג לקבץ actions יחד ולתת להם שם קבוצה בסוגריים מרובעים ושם הקבוצה (בדוגמא לעיל [GroupName]).
בנוסף ניתן להוסיף נתונים אופציונליים עם ה action.

תרגיל לתלמיד - createAction

זמן לתרגול על ה api של createAction.

  • ניתן לרשום קוד בעורך הקוד כאן באתר (שימו לב שהעורך מתחיל עם שגיאה כי הקוד לא הושלם).
  • האפליקציה האנגולרית הזו מכילה רק קומפוננטה אחת app.component.ts שתציג רשימת ריפוזיטוריות מגיטהאב.
  • האפליקציה מכילה actions.ts שם תצטרכו ליצור action יחיד.
  • האפליקציה מכילה reducer.ts שמטפל ב action שתיצרו.
  • הקובץ repos.service.ts מכיל שירות שיביא את הריפוזיטוריות מגיטהאב.
  • הקבצים שתצטרכו לשנות בתרגיל הם: app.component.ts, actions.ts ואולי reducer.ts, שאר הקבצים יהיו קריאה בלבד.
  • המטרה שה app.component.ts תציג את הרשימת הריפוזיטוריות מגיטהאב, אך תקבל את הרשימה מה store.
  • ה app.component.ts תממש את הפונקציה ngOnInit ותשתמש בשירות repos.service.ts כדי לקבל את הריפוזיטוריות ולאחר מכן תשלח action להכניס את הריפוזיטוריות ל store.
  • תצטרכו גם ליצור action ב actions.ts שיקרא מ app.component.ts ויטפל בו ה reducer שנמצא ב reducer.ts.
import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core";
import { Store } from "@ngrx/store";
import { State } from "./state";
import { RepoService } from "./repo.service";
import { setRepos } from "./actions";

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-root",
template: `
  <h1>Display the list of repos2</h1>
  <ul>
    <li *ngFor="let repo of repos$ | async">
			{{ repo.name }}
		</li>
  </ul>
`,
})
// TODO: should implement OnInit
export class AppComponent {
// TODO: should have a repos$ observable using the store.select

// TODO: should have a constructor that injects the store and the repo service

// TODO: should impelment ngOnInit and use the repo service to fetch the repos and then dispatch an action to set the repos in the store.  
}

פתרון

להלן הפתרון לתרגיל:

import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core";
import { Store } from "@ngrx/store";
import { State } from "./state";
import { RepoService } from "./repo.service";
import { setRepos } from "./actions";

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-root",
template: `
  <h1>Display the list of repos2</h1>
  <ul>
    <li *ngFor="let repo of repos$ | async">
			{{ repo.name }}
		</li>
  </ul>
`,
})
export class AppComponent implements OnInit {
repos$ = this._store.select((state) => state.repos.list);

constructor(
  private _store: Store<State>,
  private _repo: RepoService,
) {}

ngOnInit(): void {
  this._repo.getRepos().subscribe((repos) => {
    this._store.dispatch(setRepos({ repos }));
  });
}
}

  • בקובץ actions.ts אנחנו מגדירים action באמצעות createAction, אנחנו משתמשים ב props כדי לציין שה action שלנו צריך את הריפוזיטוריות שהתקבלו.
  • בקובץ app.component.ts בפונקציה ngOnInit אנחנו מביאים את הריפוזיטוריות ושולחים את ה action שיצרנו ל store.
  • בקובץ app.component.ts אנחנו משתמשים ב store.select כדי לבחור את הריפוזיטוריות מה store.
  • בקובץ reducer.ts אנחנו מטפלים ב action שלנו ומכניסים את הריפוזיטוריות ל state.

createActionGroup

ה createActionGroup משמש ליצירת קבוצה של actions בו זמנית, זה נוח כאשר יש לנו קבוצה של actions שקשורות זו לזו (בדרך כלל יש לנו קבוצות של actions ולכן תשתמשו בפונקציה זו יותר מ createAction).

import { createActionGroup, props } from '@ngrx/store';
import type { Repo } from './repo';
export const repoActions = createActionGroup({
source: 'Repos',
events: {
'Set Repos': props<{ repos: Repo[] }>(),
},
})

ה createActionGroup מקבל אובייקט עם 2 פרופרטיז:

  • source - שם הקבוצה של ה actions (בדרך כלל שם זה קשור לאיזור ב state שה actions מתייחסות אליו).
  • events - אובייקט עם ה actions, המפתח הוא שם ה action והערך הוא הפרופס של ה action.

תרגיל לתלמיד - createActionGroup

בתרגיל זה תצטרכו ליצור קבוצת actions באמצעות createActionGroup.

  • בקובץ actions.ts תצטרכו ליצור קבוצת actions שמכילה 2 actions:
    • Set Repos - תשמש להכניס את הריפוזיטוריות ל store (כמו בתרגיל הקודם).
    • Delete Repo - תשמש למחוק ריפוזיטוריה מה store (תקבל את המזהה של הריפוזיטוריה ב payload).
  • בקובץ reducer.ts תצטרכו לטפל ב action של Delete Repo ולהסיר את הריפוזיטוריה מה store בהתבסס על המזהה שקיבלתם.
  • בקובץ app.component.ts תצטרכו לשלוח את ה action של Delete Repo כאשר המשתמש לוחץ על כפתור מחיקה.
import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core";
import { Store } from "@ngrx/store";
import { State } from "./state";
import { RepoService } from "./repo.service";
import { repoActions } from "./actions";

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-root",
template: `
  <h1>Display the list of repos2</h1>
  <ul>
    <li *ngFor="let repo of repos$ | async">
      {{ repo.name }} <button (click)="deleteRepo(repo.id)">X</button>
    </li>
  </ul>
`,
})
export class AppComponent implements OnInit {
repos$ = this._store.select((state) => state.repos.list);

constructor(
  private _store: Store<State>,
  private _repo: RepoService,
) {}

ngOnInit(): void {
  this._repo.getRepos().subscribe((repos) => {
    this._store.dispatch(repoActions.setRepos({ repos }));
  });
}

deleteRepo(id: number) {
  // TODO: dispatch the delete repo action
}
}

פתרון

import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core";
import { Store } from "@ngrx/store";
import { State } from "./state";
import { RepoService } from "./repo.service";
import { repoActions } from "./actions";

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-root",
template: `
  <h1>Display the list of repos2</h1>
  <ul>
    <li *ngFor="let repo of repos$ | async">
      {{ repo.name }} <button (click)="deleteRepo(repo.id)">X</button>
    </li>
  </ul>
`,
})
export class AppComponent implements OnInit {
repos$ = this._store.select((state) => state.repos.list);

constructor(
  private _store: Store<State>,
  private _repo: RepoService,
) {}

ngOnInit(): void {
  this._repo.getRepos().subscribe((repos) => {
    this._store.dispatch(repoActions.setRepos({ repos }));
  });
}

deleteRepo(id: number) {
  this._store.dispatch(repoActions.deleteRepo({ id }));
}
}

סיכום

Actions מאפשרים לנו לספר ל NGRX על משהו שקרה באפליקציה שעשוי לגרום לשינוי ב state.
למדנו על 2 ה api’s שמספק NGRX ליצירת action (createActionGroup ו createAction).
במרבית המקרים תצטרכו ליצור קבוצת actions ולכן תשתמשו ב createActionGroup.