ngx-request-lock
v1.1.1
Published
Angular library for binding UI flows to HTTP request lifecycles. A shared requestId coordinates buttons, forms, and panels, unlocking them automatically when requests settle.
Downloads
1,319
Maintainers
Readme
ngx-request-lock
UI locking bound to the lifecycle of your HTTP requests, for Angular.
ngx-request-lock binds a UI flow to the lifecycle of its HTTP requests. A shared requestId coordinates buttons, forms, and panels across single or chained requests, unlocking them automatically when all calls complete.
Table of contents
- Why this library
- Requirements
- Compatibility
- Installation
- Setup
- Usage
- Public API
- What this library is not
- Links
- License
Why this library
Front-end state bugs often stem from active UI elements during pending HTTP requests. Unblocked controls allow repeated clicks and concurrent edits, sending duplicate requests to the server and causing state divergence.
ngx-request-lock uses requestId as the unit of coordination. Interactive elements and HTTP requests share an ID to form a reference-counted flow. The interceptor manages state through Angular primitives (HttpContext, HttpInterceptorFn, signals) without external state managers or RxJS code.
Requirements
- Angular v22 or newer.
- Peer dependencies:
@angular/common ^22.0.0,@angular/core ^22.0.0. - Standalone APIs, functional HTTP interceptors, and signals (default in v22).
- Secure context (HTTPS or
localhost) forcrypto.randomUUID().
Compatibility
| ngx-request-lock | Angular |
| ------------------ | --------- |
| 1.x | ^22.0.0 |
Installation
npm install ngx-request-lockThe library ships as Angular Package Format, is tree-shakable, and is marked sideEffects: false. The only runtime dependency is tslib.
Setup
Register the provider in your application config:
import { ApplicationConfig } from '@angular/core';
import { provideRequestLock } from 'ngx-request-lock';
export const appConfig: ApplicationConfig = {
providers: [provideRequestLock()],
};provideRequestLock() calls provideHttpClient(withInterceptors([requestLockInterceptor])) internally. Do not add a separate provideHttpClient(...) alongside it, or the interceptor will be overridden.
If your app already configures provideHttpClient with other interceptors, skip provideRequestLock() and register the interceptor directly:
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { requestLockInterceptor } from 'ngx-request-lock';
providers: [
provideHttpClient(
withInterceptors([requestLockInterceptor /*, ...others */]),
),
];Usage
Basic: one button, one request
Place the directive on the interactive element and tag the request with the same id:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
RequestLockDirective,
createRequestLockContext,
} from 'ngx-request-lock';
@Component({
selector: 'app-ping',
imports: [RequestLockDirective],
template: `
<button ngxRequestLock #lock="requestLock" (click)="ping(lock.requestId())">
Ping
</button>
`,
})
export class Ping {
private readonly http = inject(HttpClient);
protected ping(id: string): void {
this.http
.get('/api/ping', { context: createRequestLockContext(id) })
.subscribe();
}
}The button is disabled from the click until the request settles (success or error). Two safety timeouts also unblock the element:
- 500 ms if no pending state has been observed by then.
- 10 s unconditionally.
Shared flow: many elements, many requests
Bind the same requestId to every directive and every request that participates in the same flow:
@Component({
imports: [RequestLockDirective],
template: `
<button ngxRequestLock [requestId]="flowId()" (click)="save()">Save</button>
<button ngxRequestLock [requestId]="flowId()" (click)="reset()">
Reset
</button>
`,
})
export class Editor {
private readonly http = inject(HttpClient);
protected readonly flowId = signal(crypto.randomUUID());
protected save(): void {
this.http
.post('/api/items', payload, {
context: createRequestLockContext(this.flowId()),
})
.subscribe();
}
protected reset(): void {
/* ... */
}
}Every request that carries the same id contributes to one reference-counted lock. Both buttons stay disabled until every request in the flow has settled.
Pending state: swap label, show spinner
RequestLockService.isPending(id) returns a Signal<boolean> you can consume anywhere:
import { computed, inject, viewChild } from '@angular/core';
import { RequestLockDirective, RequestLockService } from 'ngx-request-lock';
private readonly service = inject(RequestLockService);
private readonly lock = viewChild.required(RequestLockDirective);
protected readonly isPending = computed(() =>
this.service.isPending(this.lock().requestId())(),
);Use it to swap a button label, render a spinner, dim a panel, or set [attr.aria-busy] on a wrapper. isPending(id) returns a new computed on every call, so store it in a field if you read it repeatedly.
Public API
| Export | Kind | Purpose |
| ------------------------------ | ---------------------------------- | ------------------------------------------------------------------- |
| REQUEST_LOCK_ID | HttpContextToken<string \| null> | Tags a request with a lock identifier. Default is null. |
| createRequestLockContext(id) | (id: string) => HttpContext | Builds the HttpContext for a tracked request. |
| requestLockInterceptor | HttpInterceptorFn | Reads the id from the context and drives the service. |
| RequestLockService | Root-provided service | Reference-counted pending state, isPending(id): Signal<boolean>. |
| RequestLockDirective | Standalone directive | Selector [ngxRequestLock], exportAs requestLock. |
| provideRequestLock() | () => EnvironmentProviders | Registers provideHttpClient(withInterceptors([...])) in one call. |
What this library is not
- It does not cancel or debounce requests.
- It does not replace HTTP-level idempotency on the server.
- It does not implement a global spinner or toast system.
- It does not ship any CSS.
Links
- Repository: github.com/SalvatoreDiGenua/ngx-request-lock-docs
- Issues: github.com/SalvatoreDiGenua/ngx-request-lock-docs/issues
- Changelog: CHANGELOG.md
- Documentation site: DOCS
License
MIT (c) 2026.
