angularserviceobservablesignalsapollo

Error in Angular components shows loading and not loading using if-else


im learning angular 18 SSR. I have a render problem when i load data from a graphql service; the load elements (skeletons) are showed using the value loading from a signal converted from a service observable method; the problem is when i render, it shows skeletons and items at same time (when the load data ends, only shows items):

enter image description here

I dont understand why occurs that.

product.service.ts

@Injectable({
 providedIn: 'root'
})

export class ProductsService {
  private apollo = inject(Apollo);

  getProductsPaginated(quantity:number, page:number): Observable<ProductResponse>{
   return this.apollo.watchQuery({
      query: GET_PRODUCTS,
      variables: { quantity, page }
    }).valueChanges.pipe(
      map(({ data, error }: any) => ({
       loading: false,
       products: data.productsPaginated.nodes,
       error: error
    }))
   );
  }
 }

shop.component.ts:

@Component({
 selector: 'app-shop',
 standalone: true,
 imports: [SidebarMenuComponent, ProductCardComponent, ProductCardSkeletonComponent],
 templateUrl: './shop.component.html',
 styleUrl: './shop.component.css'
})

export default class ShopComponent{
  public skeletonProductsLenght = [1, 2, 3, 4, 5, 6];
  public productsService = inject(ProductsService);

  public productsPaginatedResp = 
    toSignal(this.productsService.getProductsPaginated(12,1),
    { initialValue: { loading: true, products: [], error: false } });
}

shop.component.html:

 <div class="grid md:grid-cols-3 grid-cols-2 gap-6 p-4">
  @if(this.productsPaginatedResp()?.loading){
    @for(item of skeletonProductsLenght; track item){
      <app-product-card-skeleton/>
    }
  }
  @else {
    @for (item of this.productsPaginatedResp()?.products; track item.key) {
      <app-product-card [product]="item"/>
    }
  }
</div>

Solution

  • I think it might be due to SSR, you can make the code execute only on the browser using defer. So that only once the HTML renders and either it will be skeleton or products.

    Defer Docs

    @defer {
        <div class="grid md:grid-cols-3 grid-cols-2 gap-6 p-4">
          @if(this.productsPaginatedResp()?.loading){
            @for(item of skeletonProductsLenght; track item){
              <app-product-card-skeleton/>
            }
          }
          @else {
            @for (item of this.productsPaginatedResp()?.products; track item.key) {
              <app-product-card [product]="item"/>
            }
          }
        </div>
    }