Skip to main content

Angular 22 integration

The /angular adapter supports all stable Angular 22 releases, with the optional @angular/core peer set to ^22.0.0. Angular 21 and earlier, Angular 23, and prereleases are outside this support range. Keep your application's Angular runtime and compiler packages aligned to the same release.

Development dependencies and the example stay pinned to Angular 22.2.3; the example uses matching CLI/build/compiler packages and TypeScript 6.0.3. Compatibility checks install the packed library into isolated consumers using Angular 22.0.0 and 22.2.3, compile the shared fixture with each version's compiler, and test timing, inputs, computed values, keyed lists, zoneless rendering, and cleanup. CI runs these six scenarios in Chromium for both versions; the normal source suite also runs in Firefox and WebKit.

Signals, inputs, and lists​

Call injectStore(store) in an Angular injection context, such as a component field initializer, constructor, or provider factory. It returns a readonly Signal<Snapshot<T>>. Read state() in templates and Angular computed() functions; write through the original store or its methods.

import {ChangeDetectionStrategy, Component, computed} from '@angular/core';
import {createClassyStore} from '@codebelt/classy-store';
import {injectStore} from '@codebelt/classy-store/angular';

class FormStore {
text = 'Hello';
enabled = true;
items = [{id: 1, label: 'First item'}];

get characterCount() {
return this.text.length;
}

setText(text: string) {
this.text = text;
}
}

const formStore = createClassyStore(new FormStore());

@Component({
selector: 'app-form',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<label>
Text
<input [value]="state().text" (input)="setText($event)" />
</label>
<label>
<input type="checkbox" [checked]="state().enabled"
(change)="setEnabled($event)" />
Enabled
</label>
<p>{{ uppercase() }} ({{ state().characterCount }} characters)</p>
<ul>
@for (item of state().items; track item.id) {
<li>{{ item.label }}</li>
}
</ul>
`,
})
export class FormComponent {
readonly state = injectStore(formStore);
readonly uppercase = computed(() => this.state().text.toUpperCase());

setText(event: Event) {
formStore.setText((event.target as HTMLInputElement).value);
}

setEnabled(event: Event) {
formStore.enabled = (event.target as HTMLInputElement).checked;
}
}

Snapshots are immutable. Do not bind [(ngModel)] to a snapshot field or try to call .set() on the returned signal. Using stable, unique IDs in @for preserves retained rows when arrays are reordered or replaced. Nested edits, insertions, and removals through the store also update the snapshot.

Timing and zoneless rendering​

Store notifications update the Angular signal synchronously. Immediately after an action, reading state() or a dependent computed() returns current values. Angular still schedules and coalesces effects and DOM rendering. Multiple writes are not a transaction: imperative reads between writes can see intermediate state. Ordinary subscribe(store, listener) callbacks still batch in a microtask.

Angular 22 uses zoneless rendering by default. Bootstrap the standalone component with bootstrapApplication(FormComponent) from @angular/platform-browser; Zone.js is unnecessary. Signals read by a template notify Angular even when a store changes from an external timer. The adapter does not force change detection. When testing scheduled rendering, wait for ApplicationRef.whenStable() or ComponentFixture.whenStable() instead of masking scheduling with manual detectChanges() calls.

Cleanup and migration​

The subscription belongs to the injected DestroyRef. A component-scoped call unsubscribes when that component is destroyed; a service or provider factory uses its injector's lifetime. A root-provided service therefore keeps its subscription until the application is destroyed. Separate calls subscribe independently. Remounting reads the store's current state.

Calls outside an injection context or after its destruction throw without registering a listener. Destroyed consumers ignore pending notifications, including destruction during an in-progress store notification.

To migrate, update your Angular packages to the same stable Angular 22 release and use its compatible TypeScript toolchain. The injectStore(store) signature and /angular import path are unchanged. Signal reads now update immediately rather than waiting for the core subscription microtask. There is no adapter timing option.

See the Angular example for a zoneless application with inputs, a class getter, Angular computed state, and keyed lists. Angular documents the zoneless scheduler and DestroyRef lifecycle.