angulartypescriptangular-signalsangular20angular-zoneless

Getting error while convert existing code to use signal in HttpClient put method (Angular 20)


I am new to Angular. I started with Angular 19 that were using zone.js for change detection. But then I moved to Angular 20 and now I am using zoneless. I have a put method call that I am trying to convert using Signals. Here is the code without using signal.

Cart.ts

export interface Cart {
    id: number;
    products: { productId: number }[];
}

CartService:

@Injectable({
    providedIn: 'root'
})
export class CartService {

    // cart property to keep a local cache of the user cart
    cart: Cart | undefined;

    addProduct(id: number): Observable<Cart> {

        const cartProduct = { productId: id, quantity: 1 };

        return defer(() =>
            !this.cart
            ? this.httpClient.post<Cart>(this.fakeStoreCartUrl, { products: [cartProduct] })
            : this.httpClient.put<Cart>(`${this.fakeStoreCartUrl}/${this.cart.id}`, {
                products: [
                    ...this.cart.products,
                    cartProduct
                ]
            })
        ).pipe(map(cart => this.cart = cart));
    }
}

then there is a guard which looks like:

export const checkoutGuard: CanDeactivateFn<CartComponent> = (component, currentRoute, currentState, nextState) => {

    const dialog = inject(MatDialog);
    const cartService = inject(CartService);

    if (cartService.cart) {

        const confirmation = dialog.open(
            CheckoutComponent,
           { data: cartService.cart.products.length }
        ).afterClosed();

        return confirmation;
    }

    return true;

};

Here is the route in which it is using.

export const routes: Routes = [
    ....
    {
        path: 'cart',
        component: CartComponent,
        canActivate: [authGuard],
        canDeactivate: [checkoutGuard]
    },
    ...
]

After converting to zoneless I noticed that this checkoutGuard is no longer working. I tried to convert it to using Signal so change detection work. But I am getting an error. Here is what I did:

export class CartService {
    
    cart = signal<Cart | undefined>(undefined);

    addProduct(id: number): Observable<Cart> {

        const cartProduct = { productId: id, quantity: 1 };

        return defer(() =>                       // error
            !this.cart
            ? this.httpClient.post<Cart>(this.fakeStoreCartUrl, { products: [cartProduct] })
            : this.httpClient.put<Cart>(`${this.fakeStoreCartUrl}/${this.cart()?.id}`, {
                products: [
                    ...this.cart()?.products,   // error
                    cartProduct
                ]
            })
        ).pipe(map(cart => this.cart.set(cart)));
    }   
}

I also change the checkoutGuard.ts

export const checkoutGuard: CanDeactivateFn<CartComponent> = (component, currentRoute, currentState, nextState) => {

    const dialog = inject(MatDialog);
    const cartService = inject(CartService);

    // Access a signal within the component to check for unsaved changes
    if (cartService.cart()) {

       const confirmation = dialog.open(
          CheckoutComponent,
          { data: cartService.cart()?.products.length }
        ).afterClosed();

        return confirmation;
    }

    return true;
};

But now I am getting errors at in CartService

...this.cart()?.products, --> Type '{ productId: number; }[] | undefined' must have a '[Symbol.iterator]()' method that returns an iterator.ts(2488)

At return

Type 'Observable<void>' is not assignable to type 'Observable<Cart>'. Type 'void' is not assignable to type 'Cart'.ts(2322)

I am checking ...this.cart()?.products, with Optional Chaining. All other places it is working but at this line I am getting error.

Also after setting value to signal. Error is return type is Observable instead of Observable. May be there is a better solution but as I said I am new to Angular so I want to understand the cause of error as well as if there is a better solution.

Thanks


Solution

  • We should always execute the signal, before checking the value inside. Because the signal without execution is always evaluated to true.

    So the error is because the signal !this.cart would always take us to the else portion, which after execution the signal, has a chance of the signal being undefined which gives you the error.

    return defer(() =>
      // error
      !this.cart() // signal without execution will always evaluate to true, so execute it
    

    Then even if the cart is set there is a possibility of products being undefined, so I added a default value [] using the or operator.

        : this.httpClient.put<Cart>(
            `${this.fakeStoreCartUrl}/${this.cart()?.id}`,
            {
              products: [
                ...(this.cart()?.products || []), // even if the cart is set there is a possibility of products being undefined, so I added a default value.
                cartProduct,
              ],
            }
          )
    

    Finally map operator always expects a return value, but we just set the signal and returned nothing hence the error.

    You can also use ! to asset to typescript that the value will always be defined.

    ).pipe(
      map((cart) => {
        this.cart.set(cart);
        return this.cart(); // maps always expects a return value, but we just set the signal and returned nothing hence the error.
        // or below code to indicate the card will always be defined.
        // return this.cart()!;
      })
    );
    

    Full Service Code:

    export interface Cart {
      id: number;
      products: { productId: number }[];
    }
    
    @Injectable({ providedIn: 'root' })
    export class CartService {
      httpClient = inject(HttpClient);
      cart = signal<Cart | undefined>(undefined);
      fakeStoreCartUrl = 'asdf';
      addProduct(id: number): Observable<Cart | undefined> {
        const cartProduct = { productId: id, quantity: 1 };
    
        return defer(() =>
          // error
          !this.cart() // signal without execution will always evaluate to true, so execute it
            ? this.httpClient.post<Cart>(this.fakeStoreCartUrl, {
                products: [cartProduct],
              })
            : this.httpClient.put<Cart>(
                `${this.fakeStoreCartUrl}/${this.cart()?.id}`,
                {
                  products: [
                    ...(this.cart()?.products || []), // even if the cart is set there is a possibility of products being undefined, so I added a default value.
                    cartProduct,
                  ],
                }
              )
        ).pipe(
          map((cart) => {
            this.cart.set(cart);
            return this.cart(); // maps always expects a return value, but we just set the signal and returned nothing hence the error.
            // or below code to indicate the card will always be defined.
            // return this.cart()!;
          })
        );
      }
    }