Skip to content

Problem Statement

Front-end state bugs often stem from active UI elements during pending HTTP requests. Unblocked buttons or forms allow repeated clicks and concurrent edits, sending duplicate requests to the server and causing state divergence

  • Duplicate POST/PUT/DELETE requests from repeated interactions.
  • Follow-up refresh calls racing the mutation they reflect.
  • Related actions (Save + Reset, Submit + Refresh) remaining clickable during active requests.
  • Ad hoc loading booleans scattered across components.
  • Loading flags stuck at true when an error path skips resetting them.
  • Disabled state logic drifting out of sync with the HTTP request lifecycle.

Toggling component-level booleans works for single requests, but degrades quickly as complexity grows. Sequential refreshes, related actions, or panel states require separate flags that must be manually reset on every error path

typescript
          
            
              @Component({
            
                selector: 'ngx-save-user',
            
                template: `
            
                  <button (click)="save()" [disabled]="loading">Save</button>
            
                `,
            
              })
            
              export class SaveUser {
            
                private readonly http = inject(HttpClient);
            
                protected loading = false;
            
              
            
                protected save(): void {
            
                  this.loading = true;
            
                  this.http.post('/api/users', this.form.value).subscribe({
            
                    next: () => (this.loading = false),
            
                    error: () => (this.loading = false),
            
                  });
            
                }
            
              }
            
        

A shared requestId tracks the entire flow through HttpContext. An interceptor updates a reference-counted signal service: all directives and wrappers bound to that ID unlock automatically when all requests settle, including on error

typescript
          
            
              @Component({
            
                selector: 'ngx-save-user',
            
                imports: [RequestLockDirective],
            
                template: `
            
                  <!--
            
                    One shared `requestId` coordinates the whole flow:
            
                    both buttons and the follow-up GET are locked together
            
                    and only re-enable when everything settles.
            
                  -->
            
                  <button
            
                    ngxRequestLock
            
                    [requestId]="flowId()"
            
                    (click)="save()"
            
                  >
            
                    Save
            
                  </button>
            
              
            
                  <button
            
                    ngxRequestLock
            
                    [requestId]="flowId()"
            
                    (click)="refresh()"
            
                  >
            
                    Refresh
            
                  </button>
            
                `,
            
              })
            
              export class SaveUser {
            
                private readonly http = inject(HttpClient);
            
                protected readonly flowId = signal(crypto.randomUUID());
            
              
            
                protected save(): void {
            
                  const id = this.flowId();
            
                  this.http
            
                    .post('/api/users', this.form.value, {
            
                      context: createRequestLockContext(id),
            
                    })
            
                    // Automatic follow-up: same id, same lock.
            
                    .subscribe(() => this.refresh());
            
                }
            
              
            
                protected refresh(): void {
            
                  this.http
            
                    .get('/api/users', {
            
                      context: createRequestLockContext(this.flowId()),
            
                    })
            
                    .subscribe();
            
                }
            
              }
            
        

  • Does not cancel or debounce requests.
  • Does not replace server-side HTTP idempotency.
  • Does not provide global spinners or toast notifications.

ngx-request-lock

UI locking bound to HTTP request lifecycle for Angular applications

© 2026 Salvatore Di Genua - Built with Angular and Tailwind CSS