Angular 7中ngIf首次生效后后续失效问题求助
*ngIf Loading State Stops Working After the First Click in Angular 7 Let's break down what's going wrong with your loading state and fix it step by step.
Issue Analysis
Your code has a few key issues that cause the loading state to behave unexpectedly after the first submission:
Duplicate Subscriptions Pile Up
Every time you callonSubmit(), you add a new subscription to yourProductService(aBehaviorSubject). This means:- When the service emits new data, all existing subscriptions will fire
this.loading = false—leading to race conditions. - New subscriptions immediately receive the latest value from the
BehaviorSubject, soloadinggets set totrueand then instantly reset tofalsebefore the UI can update.
- When the service emits new data, all existing subscriptions will fire
Misaligned Subscription with Request Trigger
Your current component code setsloading = trueand subscribes to the service, but it doesn’t explicitly trigger a new request (I assume you’re callingthis.Service.query()somewhere, but even then, the subscription approach is flawed). TheBehaviorSubjectis meant for sharing data, not tracking individual request states.Uncleaned Subscriptions Cause Memory Leaks
If you don’t unsubscribe when the component destroys, you’ll have lingering subscriptions that can cause unexpected state changes and memory leaks.
Solutions
We’ll cover two approaches—using Angular’s AsyncPipe (recommended) for automatic subscription management, and manual subscription cleanup for simpler use cases.
Solution 1: Use AsyncPipe (Angular Best Practice)
AsyncPipe handles subscribing and unsubscribing automatically, eliminating memory leaks and subscription bloat.
Component Code Changes
import { Subject, BehaviorSubject, tap, mergeMap, finalize } from 'rxjs'; import { ProductService, ProductCatalogSearch, GridDataResult } from './path-to-your-service'; import { FormGroup } from '@angular/forms'; export class YourComponent implements OnInit, OnDestroy { // Trigger to fire submission events private submitTrigger$ = new Subject<void>(); // Observable to track loading state (AsyncPipe will subscribe to this) public loading$ = new BehaviorSubject<boolean>(false); public catalogForm: FormGroup; constructor(private productService: ProductService) {} ngOnInit() { this.submitTrigger$ .pipe( // Set loading to true when submission starts tap(() => this.loading$.next(true)), // Trigger the actual request mergeMap(() => { // Replace with your actual state and form values const requestState = {}; const searchForm = this.catalogForm.value as ProductCatalogSearch; const customerGroupCode = 'your-customer-group-code'; // Use the fetch method directly to get the request observable return this.productService.fetch(requestState, searchForm, customerGroupCode); }), // Reset loading when request completes (success OR failure) finalize(() => this.loading$.next(false)) ) .subscribe((response: GridDataResult) => { // Update the service's BehaviorSubject with new data this.productService.next(response); // Add any other post-request logic here }); } onSubmit() { // Emit a trigger to start the submission flow this.submitTrigger$.next(); } ngOnDestroy() { // Clean up the subject to prevent memory leaks this.submitTrigger$.complete(); } }
HTML Template Changes
Use the async pipe to bind directly to the loading$ observable:
<form class="k-form-inline" [formGroup]="catalogForm" (ngSubmit)="onSubmit()" (keyup.enter)="onSubmit()"> <button class="k-button k-primary" style="width:100px; margin-right:15px" [disabled]="loading$ | async" > <span class="k-icon k-i-zoom" style="padding-right: 20px"></span> Search </button> <span *ngIf="loading$ | async" class="k-icon k-i-loading" style="font-size: 16px; padding-left:10px" ></span> </form>
Solution 2: Manual Subscription Management
If you prefer to use a boolean variable for loading, make sure to clean up subscriptions and tie the loading state directly to individual requests.
Component Code Changes
import { Subscription } from 'rxjs'; import { ProductService, ProductCatalogSearch, GridDataResult } from './path-to-your-service'; import { FormGroup } from '@angular/forms'; export class YourComponent implements OnInit, OnDestroy { public loading: boolean = false; private requestSubscription?: Subscription; public catalogForm: FormGroup; constructor(private productService: ProductService) {} onSubmit() { this.loading = true; // Cancel any existing request before starting a new one this.requestSubscription?.unsubscribe(); // Replace with your actual request parameters const requestState = {}; const searchForm = this.catalogForm.value as ProductCatalogSearch; const customerGroupCode = 'your-customer-group-code'; // Subscribe directly to the request observable this.requestSubscription = this.productService.fetch(requestState, searchForm, customerGroupCode) .subscribe({ next: (response: GridDataResult) => { this.productService.next(response); this.loading = false; }, error: (error) => { // Always reset loading on error! console.error('Request failed:', error); this.loading = false; } }); } ngOnDestroy() { // Clean up the subscription when component is destroyed this.requestSubscription?.unsubscribe(); } }
HTML Template
Keep your original template—no changes needed here:
<form class="k-form-inline" [formGroup]="catalogForm" (ngSubmit)="onSubmit()" (keyup.enter)="onSubmit()"> <button class="k-button k-primary" style="width:100px; margin-right:15px" [disabled]="loading" > <span class="k-icon k-i-zoom" style="padding-right: 20px"></span>Search </button> <span *ngIf="loading" class="k-icon k-i-loading" style="font-size: 16px; padding-left:10px"></span> </form>
Additional Tips
- Handle Errors: Always reset the loading state in error callbacks—otherwise, your loading icon will stay visible if the request fails.
- Separate Data Sharing from Request State: Use
BehaviorSubjectfor sharing data across components, but tie loading state directly to individual request observables. - Use RxJS Operators:
tap,finalize, andmergeMapmake your code cleaner and reduce repetitive logic.
内容的提问来源于stack exchange,提问作者Dips

