Search code examples
javascriptangulartypescriptangular-componentspage-refresh

How to update a component in Angular using a backend request, without refreshing the page?


I want to update my heart icon, which bound to every product, to add to favourites.

  <i class="fa-regular fa-heart" *ngIf="!isFavourite"></i>
  <i class="fa-solid fa-heart" *ngIf="isFavourite"></i>

To do it I fetch data from the database using a backend request in the service, same with toggling, the backend logic works, the only problem is that I have to refresh the page every time to see the heart icon toggled.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../../../environments/environment';
import { Observable, BehaviorSubject } from 'rxjs';
import { PrimitiveProduct } from '../models/product.model';

@Injectable({
  providedIn: 'root'
})
export class FavouriteService {
  apiUrl = `${environment.apiUrl}/user/favourites`;

  private favouritesSubject = new BehaviorSubject<PrimitiveProduct[]>([]);
  favourites$ = this.favouritesSubject.asObservable();

  constructor(private http: HttpClient) {}

  fetchFavourites(): void {
    this.http
      .get<PrimitiveProduct[]>(this.apiUrl, { withCredentials: true })
      .subscribe({
        next: (favourites) => {
          this.favouritesSubject.next(favourites);
        },
        error: (err) => console.error('Error fetching favourites:', err)
      });
  }

  toggleFavourite(productUid: string): Observable<string> {
    return this.http.post<string>(
      `${this.apiUrl}?productUid=${productUid}`,
      null,
      { withCredentials: true }
    );
  }

  // edited
  updateFavouritesLocally(productUid: string) {
    const favourites = this.favouritesSubject.getValue();
    const isAlreadyFavorite = favourites.some((fav) => fav.uid === productUid);

    if (isAlreadyFavorite) {
      this.favouritesSubject.next(
        favourites.filter((fav) => fav.uid !== productUid)
      );
    } else {
      this.productService.getProductByUid(productUid).subscribe({
        next: (fullProduct) => {
          if (fullProduct) {
            this.favouritesSubject.next([...favourites, fullProduct]);
          } else {
            console.error(`Product with uid ${productUid} not found`);
          }
        },
        error: (err) => {
          console.error(`Error fetching product with uid ${productUid}:`, err);
        }
      });
    }
  }
}

This is how I use the toggleFavourite() in product component:

  ngOnInit() {
    this.favouriteService.favourites$.subscribe((favourites) => {
      this.isFavourite = favourites.some((fav) => fav.uid === this.product.uid);
    });
  }

  toggleFavourite(event: Event) {
    event.stopPropagation();

    this.favouriteService.toggleFavourite(this.product.uid).subscribe({
      next: () => {
        this.favouriteService.updateFavouritesLocally(this.product.uid);
      },
      error: (err) => {
        console.error('Error toggling favourite:', err);
      }
    });
  }

And here in favourites component I display the data in user account section:

export class FavouritesComponent implements OnInit {
  favouriteProducts: PrimitiveProduct[] = [];

  constructor(private favouriteService: FavouriteService) {}

  ngOnInit() {
    this.favouriteService.fetchFavourites();
    this.favouriteService.favourites$.subscribe((products) => {
      this.favouriteProducts = products;
    });
  }
}

I tried the approach with BehaviorSubject, but still I have to refresh page to see the result. Overall adding products to favourites (wishlist) works fine, I just have problem with this annoying refreshing. Should I use something like localStorage or something? Looks like a simple task, but still don't know how to do it.


Solution

  • Heres a couple of free pointers:

    1. Use a dynamic class rather than ngIf. There are a few different ways you can do this, here's one:
    <i [class]="'fa-heart ' + isFavourite ? 'fa-regular' : 'fa-solid'"></i>
    
    1. ALWAYS unsubscribe from your subscriptions. Here is a resource on ways to do that.

    On to your problem: Your toggleFavourite function is too complex. There are 2 ways to solve this problem.

    1. Make the call to the backend and wait for the result then show the result
    2. Just assume it worked, update the frontend, and move on.

    You are sort of doing both.

    I'm going to assume you are trying to do number 2. In your toggleFavourite function move the call to updateFavouritesLocally out of your subscription. There is no need to wait for the toggle response because you are not using anything that is returned.

      toggleFavourite(event: Event) {
        event.stopPropagation();
    
        this.favouriteService.updateFavouritesLocally(this.product.uid);
        // Add `.pipe()` and use it to unsubscribe
        this.favouriteService.toggleFavourite(this.product.uid).subscribe({
          next: () => { /* empty */ },
          error: (err) => {
            console.error('Error toggling favourite:', err);
          }
        });
    

    If this doesn't work then its likely in your updateFavouritesLocally function. Which brings me to another note, this casting has me concerned

          const newFavourite: PrimitiveProduct = {
            uid: productUid
          } as PrimitiveProduct;
          this.favouritesSubject.next([...favourites, newFavourite]);
    

    In typescript you shouldn't have to cast like this. Either it is a PrimitiveProduct because that's the correct signature or it isn't. Could the problem be somewhere in the missing data in this object? I can't say for sure without seeing the template code too.