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
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()!;
})
);
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()!;
})
);
}
}