Angular – 不使用NgRx的简单CRUD应用程序?

huangapple go评论87阅读模式
英文:

Angular - Simple CRUD application without NgRx?

问题

I am currently learning Angular and following the Maximilian Schwarzmüller Udemy course.

I have a simple CRUD application to manage recipes with data saved in a database, and I want to be able to delete, create, and update a recipe by sending an HTTP request, and then update the list of recipes accordingly.

UI of this application

Using just RxJS BehaviorSubject and a service, I currently have this implementation to delete a recipe:

export class RecipeDetailComponent implements OnInit {
    selectedRecipe: Recipe;
  
    constructor(
      private recipeService: RecipeService,
      private activatedRoute: ActivatedRoute,
      private router: Router
    ) { }
  
    ngOnInit() {
      this.activatedRoute.params.subscribe((params) => {
        const id = params.id;
        this.recipeService.getRecipeById(id).subscribe((recipe) => {
          this.selectedRecipe = recipe;
        });
      });
    }
  
    onDeleteRecipe() {
      this.recipeService.deleteRecipe(this.selectedRecipe.id).subscribe({
        next: () => {
          this.router.navigate(['/recipes']);
          this.recipeService.recipesUpdate.next();
        },
        error: (error) => {
          console.error('error deleting recipe : ', error);
        }
      });
    }
  }
export class RecipeListComponent implements OnInit {
  recipes$: Observable<Recipe[]>;
  isLoading = false;
  errorMessage: string;
  constructor(private recipeService: RecipeService) {}

  ngOnInit() {
    this.initRecipes();
    this.recipeService.recipesUpdate.subscribe(() => this.initRecipes());
  }

  initRecipes() {
    this.isLoading = true;
    this.recipes$ = this.recipeService.getRecipes().pipe(
      catchError((error) => {
        console.error('error retrieving recipes : ', error);
        this.errorMessage = `Error retrieving recipes : ${error.error.error}`;
        return of([]);
      }),
      tap({ complete: () => (this.isLoading = false) })
    );
  }
}
export class RecipeService {
    API_URL = 'XXX';
    private recipes: Recipe[] = [];
  
    recipesUpdate: Subject<void> = new Subject<void>();
  
    recipes$ = new BehaviorSubject<Recipe[]>(this.recipes);
  
    constructor(private http: HttpClient) { }
  
    getRecipes() {
      return this.http.get<Recipe[]>(`${this.API_URL}/recipes.json`)
    }
  
    getRecipeById(id: string) {
      return this.http.get<Recipe>(`${this.API_URL}/recipes/${id}.json`)
    }
  
    addRecipe(recipe: Recipe) {
      return this.http
        .post(`${this.API_URL}/recipes.json`, recipe)
        .subscribe((response) => {
          this.recipesUpdate.next();
        });
    }
  
    updateRecipe(recipe: Recipe) {
      return this.http
        .put(`${this.API_URL}/recipes/${recipe.id}.json`, recipe)
        .subscribe((response) => {
          this.recipesUpdate.next();
        });
    }
  
    deleteRecipe(id: string) {
      return this.http.delete(`${this.API_URL}/recipes/${id}.json`);
    }
}

I'm not sure if this is the best way to do it, particularly the way I am updating the list of recipes in the RecipeListComponent using an empty Subject and subscribing to it in the ngOnInit method.

I have read many comments about NgRx and how it is often considered overkill for simple applications, but I am not sure how to do it without using it.

Furthermore, I don't like the fact that I have to "reload" the recipe list and show the loader after deleting, creating, or updating a recipe. I used React Query with React for this purpose. Is there a way to achieve the same with Angular?

BONUS QUESTION: Regarding NgRx, in my Angular course, I am almost at the part about NgRx, but I am unsure whether I should follow it or not. Do you think it is worth learning?

英文:

I am currently learning Angular and following the Maximilian Schwarzmüller Udemy course.

I have a simple CRUD application to manage recipes with data saved in a database, and I want to be able to delete, create, and update a recipe by sending an HTTP request, and then update the list of recipes accordingly.

UI of this application

Using just RxJS BehaviorSubject and a service, I currently have this implementation to delete a recipe:

export class RecipeDetailComponent implements OnInit {
    selectedRecipe: Recipe;
  
    constructor(
      private recipeService: RecipeService,
      private activatedRoute: ActivatedRoute,
      private router: Router
    ) { }
  
    ngOnInit() {
      this.activatedRoute.params.subscribe((params) =&gt; {
        const id = params.id;
        this.recipeService.getRecipeById(id).subscribe((recipe) =&gt; {
          this.selectedRecipe = recipe;
        });
      });
    }
  
  
    onDeleteRecipe() {
      this.recipeService.deleteRecipe(this.selectedRecipe.id).subscribe({
        next: () =&gt; {
          this.router.navigate([&#39;/recipes&#39;]);
          this.recipeService.recipesUpdate.next();
        },
        error: (error) =&gt; {
          console.error(&#39;error deleting recipe : &#39;, error);
        }
      });
    }
  }

export class RecipeListComponent implements OnInit {
  recipes$: Observable&lt;Recipe[]&gt;;
  isLoading = false;
  errorMessage: string;
  constructor(private recipeService: RecipeService) {}

  ngOnInit() {
    this.initRecipes();
    this.recipeService.recipesUpdate.subscribe(() =&gt; this.initRecipes());
  }

  initRecipes() {
    this.isLoading = true;
    this.recipes$ = this.recipeService.getRecipes().pipe(
      catchError((error) =&gt; {
        console.error(&#39;error retrieving recipes : &#39;, error);
        this.errorMessage = `Error retrieving recipes : ${error.error.error}`;
        return of([]);
      }),
      tap({ complete: () =&gt; (this.isLoading = false) })
    );
  }
}

export class RecipeService {
    API_URL =
      &#39;XXX&#39;;
    private recipes: Recipe[] = [];
  
    recipesUpdate: Subject&lt;void&gt; = new Subject&lt;void&gt;();
  
    recipes$ = new BehaviorSubject&lt;Recipe[]&gt;(this.recipes);
  
    constructor(private http: HttpClient) { }
  
    getRecipes() {
      return this.http.get&lt;Recipe[]&gt;(`${this.API_URL}/recipes.json`)
    }
  
    getRecipeById(id: string) {
      return this.http.get&lt;Recipe&gt;(`${this.API_URL}/recipes/${id}.json`)
    }
  
    addRecipe(recipe: Recipe) {
      return this.http
        .post(`${this.API_URL}/recipes.json`, recipe)
        .subscribe((response) =&gt; {
          this.recipesUpdate.next();
        });
    }
  
    updateRecipe(recipe: Recipe) {
      return this.http
        .put(`${this.API_URL}/recipes/${recipe.id}.json`, recipe)
        .subscribe((response) =&gt; {
          this.recipesUpdate.next();
        });
    }
  
    deleteRecipe(id: string) {
      return this.http.delete(`${this.API_URL}/recipes/${id}.json`);
    }
  }

I'm not sure if this is the best way to do it, particularly the way I am updating the list of recipes in the RecipeListComponent using an empty Subject and subscribing to it in the ngOnInit method.

I have read many comments about NgRx and how it is often considered overkill for simple applications (https://blog.angular-university.io/angular-2-redux-ngrx-rxjs/), but I am not sure how to do it without using it.

Furthermore, I don't like the fact that I have to "reload" the recipe list and show the loader after deleting, creating, or updating a recipe. I used React Query with React for this purpose. Is there a way to achieve the same with Angular?

BONUS QUESTION:
Regarding NgRx, in my Angular course, I am almost at the part about NgRx, but I am unsure whether I should follow it or not. Do you think it is worth learning?

答案1

得分: 1

首先,我想称赞您的出色的第一个问题文档。我也是从 Maximilian Schwarzmüller 开始的。

使用服务来进行状态管理是完全合理的方法。总体来说,您所做的是正确的。我建议您研究命令式代码与声明式代码的区别(Joshua Morony 在这个主题上有不错的内容)。例如,您可以使用 async 管道 直接在模板中显示食谱,避免了订阅的麻烦。

关于您所描述的加载器行为,可以通过使用乐观渲染来缓解。当用户执行 CRUD 操作时,您可以同时对本地状态和远程状态进行更改,而不显示任何加载器,在出现错误时,将本地状态回滚并向用户显示错误。

关于 RecipeListComponent 中的 "recipes$" 的细节:当可观察序列中发生错误时,catchError 运算符会被触发,它处理错误并返回一个新的可观察序列。在这种情况下,catchError 运算符返回一个空数组的可观察序列(of([]))。因此,tap 运算符将不会被触发,它的副作用,将 isLoading 设置为 false,将不会发生。

关于奖励问题:我在四个月前开始使用 ngrx,认为它是一个很好的工具。此刻,我会争辩说如果 ngrx 过于繁琐,那么 Angular 本身也是如此。一旦你练习得足够多,它并不比服务更难。NGRX 强制你创建更透明和更干净的数据流,这可以节省你大量的调试时间。但是有个但...但我认为对于刚开始学习 Angular 的人来说,从学习曲线的角度来看,ngrx 过于繁琐。我肯定会等到我对 Angular 本身有牢固的掌握之后再添加更多的东西。

希望我没有漏掉任何东西。

重构建议

在仔细查看您的代码后,我发现了一些多余的操作。我建议重构您的代码。以下是获取食谱的示例:

当我们调用 get recipes 时,首先我们将 loading 设置为 true,以通知所有监听的人食谱正在加载中。然后我们进行 API 调用本身。您可能会注意到 take(1) 管道,它的目的是在第一次发射后自动取消订阅。根据您使用的后端,它可能是不必要的,但仍然是一种防止内存泄漏的良好实践。

还有一个 finalize 管道,它将在可观察对象完成或出现错误时执行。

当收到值时,它会通过 recipes$ 发送。

export class RecipeService {
    API_URL = 'XXX';
    isLoading$ = new BehaviorSubject<boolean>(false);
    recipes$ = new BehaviorSubject<Recipe[]>([]);
    errorMessage: string;

    constructor(private http: HttpClient) { }

    getRecipes() {
        this.isLoading$.next(true);
        this.http.get(`${this.API_URL}/recipes.json`)
            .pipe(
                take(1),
                finalize(() => this.isLoading$.next(false))),
                catchError((error) => {
                    console.error('error retrieving recipes : ', error);
                    this.errorMessage = `Error retrieving recipes : ${error.error.error}`;
                    return of([]);
                }
            ).subscribe((recipes)=>{
                this.recipes$.next(recipes)
            })
    }
}

在我们的列表组件初始化时,我们执行 getRecipes() 来进行后端调用。

export class RecipeListComponent implements OnInit {
    recipes$: Observable<Recipe[]> = this.recipeService.recipes$;
    errorMessage: string;
    constructor(private recipeService: RecipeService) { }

    ngOnInit() {
        this.recipeService.getRecipes();
    }
}

模板如下:

<app-recipe-item *ngFor="let recipe of recipes$ | async"></app-recipe-item>
英文:

First of all wanted to commend you on great first question documentation. I also was starting with Maximilian Schwarzmüller.

Using services for state management is perfectly fine approach. And overall what you are doing there is fine. I would recommend to investigate imperative vs declarative code (Joshua Morony has decent content on the topic). For example, you can display recipes directly in the template using async pipe avoiding subscription hassle.

What you are describing about loader, this behavior can be mitigated by using optimistic rendering. When user performs CRUD operation you make change on local state simultaneously with remote state, not showing any loader, in case of an error you rollback local state and show user an error.

A detail regarding "recipes$" in RecipeListComponent: when an error occurs in the observable sequence, the catchError operator is triggered, and it handles the error and returns a new observable. In this case, the catchError operator returns an observable of an empty array (of([])). Therefore, the tap operator will not be reached and its side effect, setting isLoading to false, will not occur.

Regarding bonus question: I have started using ngrx 4 month ago, and consider it a great tool. At this point I would argue that if ngrx is overkill than so is angular itself. Once you have enough practice its not harder that services. NGRX forces you to create more transparent and cleaner data flows which can save you a lot of debugging time. Here comes but... but I do believe that ngrx is overkill for someone starting with angular, from learning curve point of view. I would definitely wait until I had solid grasp on Angular itself before adding on more weight.

Hope I didn't miss anything.

Refactor suggestion

After looking your code closer I saw some redundant actions. I would suggest refactoring your code. Here is fetching recipes as an example:

When we call get recipes, first of all we push loading true to inform everyone listening that recipes are being loaded. Then we make api call itself. You may notice take(1) pipe, its purpose is to automatically unsubscribe after first emission. Depending on backend you are using it may be unnecessary, be still its a good practice to prevent memory leaks.

Also we have finalize pipe which will execute when observable completes or errors.

When value is received it is sent through recipes$.

export class RecipeService {
    API_URL = &#39;XXX&#39;;
    isLoading$ = new BehaviorSubject&lt;boolean&gt;(false);
    recipes$ = new BehaviorSubject&lt;Recipe[]&gt;([]);
    errorMessage: string;

    constructor(private http: HttpClient) { }

    getRecipes() {
        this.isLoading$.next(true);
        this.http.get(`${this.API_URL}/recipes.json`)
            .pipe(
                take(1),
                finalize(() =&gt; this.isLoading$.next(false))),
                catchError((error) =&gt; {
                    console.error(&#39;error retrieving recipes : &#39;, error);
                    this.errorMessage = `Error retrieving recipes : ${error.error.error}`;
                    return of([]);
                }
            ).subscribe((recipes)=&gt;{
                this.recipes$.next(recipes)
            })
    }
}

On initialization of our list component we execute getRecipes() to make backend call.

export class RecipeListComponent implements OnInit {
    recipes$: Observable&lt;Recipe[]&gt; = this.recipeService.recipes$;
    errorMessage: string;
    constructor(private recipeService: RecipeService) { }

    ngOnInit() {
        this.recipeService.getRecipes();
    }
}

Template would be:

&lt;app-recipe-item *ngFor=&quot;let recipe of recipes$ | async&quot;&gt;&lt;/app-recipe-item&gt;

huangapple
  • 本文由 发表于 2023年6月16日 00:23:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/76483695.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定