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.
- Why this library
- Requirements
- Compatibility
- Installation
- Quick start
- Usage patterns
- Public API
- What this library is not
- Project structure
- Local development
- Issues
- Changelog
- License
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.
- 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().
ngx-request-lock |
Angular |
|---|---|
1.x |
^22.0.0 |
npm install ngx-request-lockRegister the provider in your application config:
// app.config.ts
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 */]),
),
];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.
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.
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.
| 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. |
- 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.
This repository is an Angular monorepo containing both the library and its documentation site.
.
├── projects/ngx-request-lock/ The published library source
│ ├── src/ Library entry point and public API
│ ├── package.json npm manifest (version 1.0.0)
│ ├── README.md npm-facing README
│ └── ng-package.json Angular Package Format config
├── src/ Documentation Angular app
│ ├── app/
│ │ ├── core/ Layout, i18n, navigation
│ │ ├── pages/ Route-based docs pages
│ │ └── shared/ui/ Reusable blocks (callout, code-block, ...)
│ └── styles.css Global styles and design tokens
├── public/i18n/ en.json and it.json translation files
├── public/ Static assets (favicons, manifest)
├── CHANGELOG.md Release notes
├── LICENSE MIT
└── README.md This file
The library and the docs app are versioned independently. Library releases are tracked in CHANGELOG.md.
Install dependencies:
npm installBuild the library first (the docs app imports from dist/ngx-request-lock via the workspace path mapping):
npm run build:libRun the docs app locally:
npm startThen open http://localhost:4200/.
Other useful scripts:
npm run build # Production build of the docs app
npm run build:lib # Production build of the library
npm test # Run the docs app test suite
npm run lint # Lint both projectsLibrary unit tests live under projects/ngx-request-lock/src/lib/**/*.spec.ts and run with Vitest via Angular's test builder:
npx ng test ngx-request-lock --watch=falseBug reports and feature requests are welcome on the GitHub issue tracker.
See CHANGELOG.md for the full list of changes across releases.
MIT (c) 2026.