(uh nk - ee t ah s oo d)
SoodAnkita
ankitasood.bsky.socialWebVibesOnlyGuacamoleAnkita
Re-runs change detection for entire component tree when any async event occurs.
Uses Zone.js to monkey-patch browser APIs and trigger global change detection.
Components re-evaluate even if their data hasn't changed.
Updates only the specific DOM elements that depend on changed signals.
Skips components whose signals haven't changed.
Enables zoneless Angular applications.
Evolved from being primarily
RxJS-based Observables to Signals.
Used for event handling & asynchronous programming to manage multiple values emitted over time.
Handling HTTP requests.
Routing.
Forms modules.
Event handling - Dom events, custom component events, web socket events.
Lightweight wrapper around a value that notifies interested consumers when that value changes.
class ItemCount implements OnDestroy {
count = new BehaviorSubject(0);
count$ = this.count.asObservable();
increment():void {
const curr = this.count.value;
this.count.next(curr + 1);
}
ngOnDestroy(): void {
this.count.complete();
}
}class ItemCount {
count = signal(0);
increment(): void {
this.count.update(curr => curr + 1);
}
}A Signal with a value that can be mutated.
Cannot directly assign values to it.
const count = signal(0); // Create
count.set(5); // Set directly
count.update(c => c + 1); // Update based on previousconst useDiscount = signal(false);
const price = signal(100);
const discount = signal(0.1);
const finalPrice = computed(() => {
return useDiscount() ? price() * (1 - discount()) : price();
});
const items = signal(['Apple', 'Banana', 'Cherry']);
// linked signal: always shows the first item
selected = linkedSignal(() => items()[0]);
console.log(selected()); // → 'Apple'
items.set(['Orange', 'Pear']);
console.log(selected()); // → 'Orange'items = input<string[]>([]);
// linked signal: default to first item, null if no input
selected = linkedSignal({
source: ()=> this.items(),
computation: (source, previous) => {
return source.find(i => previous?.value === i) || null;
}
});
isSelected (item: string) {
return this.selected() === item;
} @for (item of items(); track $index) {
<a (click)="selected.set(item)">{{item}}</a>
}
<span>{{selected() || "No item selected."}}</span>selectedItem = signal<Item | null>(null);
price = computed(()=> this.selectedItem()?.price ?? 0);
// quantity depends on selected Item
quantity = linkedSignal({
source: this.selectedItem,
computation: () => 1
});
total = computed(()=> this.quantity() * this.price());
onQuantityChanged(q: number) {
this.quantity.set(q);
}Re-compute when those signals change.
Operations that run when signal values change.
export class EffectiveCounter {
readonly count = signal(0);
constructor() {
// Register a new effect.
effect(() => {
console.log(`The count is: ${this.count()}`);
});
}
}Pass an Injector to create outside the constructor.
export class EffectiveCounter {
readonly count = signal(0);
private injector = inject(Injector);
initializeLogging(): void {
effect(() => {
console.log(`The count is: ${this.count()}`);
}, {injector: this.injector});
}
}Automatically destroyed when its enclosing context is destroyed.
Performing custom rendering to a canvas, charting library, or other third party UI library.
Logging.
Syncing data with window.localStorage.
Adding custom DOM behavior.
Don't use effects for state propagation!
ExpressionChangedAfterItHasBeenChecked errors.effect(() => {
console.log(`User set to ${currentUser()} and the counter is ${untracked(counter)}`);
});Prevent a signal read from being tracked by calling its getter with untracked.
Deborah Kurata's Effects decision tree: https://youtu.be/XWz8pxQWD8c?si=Xrgy7Is9ESCvoZUE
Model Inputs
InputSignal
View Queries (viewChild, viewChildren)
Content Queries (contentChild, contentChildren)
export class DateComponent {
@Input() value = new Date();
@Output() valueChange = new EventEmitter();
value = model(new Date());
}
@Component({
selector: 'app-settings-page',
template: `<h2>Dark Mode Setting</h2>
<app-toggle-switch [(checked)]="darkModeEnabled" />
<p>Dark Mode is: {{ darkModeEnabled() ? 'ON' : 'OFF' }}</p>`,
})
export class SettingsPage {
darkModeEnabled = signal(false);
}
@Component({
selector: 'app-toggle-switch',
// template: A switch UI that calls this.checked.set(!this.checked()) on click
})
export class ToggleSwitch {
checked = model(false);
toggle(): void {
this.checked.update(isCurrentlyChecked => !isCurrentlyChecked);
}
}export class ListItem {
@Input("required")title: string = “”;
title = input.required<string>();
@Input({transform: (value: string) => value.toUpperCase()}) descr: string = "Default description"
descr = input('Default description',{transform: (value: string) => value.toUpperCase()});
}
export class SearchDialog {
isOpen = false;
@ViewChild('search') searchInput: ElementRef<HTMLElement> | undefined;
searchInput = viewChild.required<ElementRef<HTMLElement>>('search');
// some additional logic goes here that updates value of isOpen
ngAfterViewInit(): void {
if (isOpen) {
this.searchInput.nativeElement.focus();
this.searchInput().nativeElement.focus();
}
}
}@Component({
selector: 'custom-toggle',
})
export class CustomToggle {
text: string;
}
@Component({
selector: 'custom-expando',
})
export class CustomExpando {
toggle = contentChild(CustomToggle);
toggleText = computed(() => this.toggle()?.text);
}
@Component({
template: `
<custom-expando>
<custom-toggle>Show</custom-toggle>
</custom-expando>
`
})
export class UserProfile { }
@Component({
selector: 'custom-toggle',
})
export class CustomToggle {
text: string;
}
@Component({
selector: 'custom-expando',
})
export class CustomExpando {
@ContentChild(CustomToggle) toggle: CustomToggle;
ngAfterContentInit() {
console.log(this.toggle.text);
}
}
@Component({
template: `
<custom-expando>
<custom-toggle>Show</custom-toggle>
</custom-expando>
`
})
export class UserProfile { }
resource(): Lower level, works with promises.
httpResource(): Works with a url string.
rxResource: Works with Observables.
Experimental APIsIncludes built-in status tracking (loading, error, success).
Similar behavior to switchMap built-in to handle consecutive requests (AbortSignal).
Easy to set default value & trigger reload.
Doesn't rely on zone.js.
const userId: Signal<string> = getUserId();
const userResource = resource({
params: () => ({id: userId()}),
loader: ({params}) => fetchUser(params),
defaultValue: ''
});
const firstName = computed(() => {
if (userResource.hasValue()) {
return userResource.value().firstName;
}
// fallback in case the resource value is `undefined` or if the resource is in error state
return undefined;
});const userId: Signal<string> = getUserId();
const userResource = resource({
params: () => ({id: userId()}),
loader: ({params, abortSignal}): Promise<User> => {
// fetch cancels any outstanding HTTP requests when the given `AbortSignal`
// indicates that the request has been aborted.
return fetch(`users/${params.id}`,
{signal: abortSignal});
},
});// programatically trigger a resource's loader
userResource.reload();Wrapper around HttpClient.
Initiates the request eagerly.
Exposes the request status and response values as a WriteableResource.
Only for retrive operations only i.e. get().
@if(user.hasValue()) {
<user-details [user]="user.value()">
} @else if (user.error()) {
<div>Could not load user information</div>
} @else if (user.isLoading()) {
<div>Loading user info...</div>
}userId = input.required<string>();
user = httpResource(() => `/api/user/${userId()}`); // A reactive function as argumentThe signals of the httpResource can be used in the template to control which elements should be displayed.
Source is defined as RxJS Observable.
Instead of a loader function, it accepts a stream function that accepts an RxJS Observable.
Same APIs as resource.
export class UserProfile {
// Relies on a service that exposes data through an RxJS Observable.
private userData = inject(MyUserDataClient);
protected userId = input<string>();
private userResource = rxResource({
params: () => ({ userId: this.userId() }),
stream: ({params}) => this.userData.load(params.userId),
});
}| Feature | resource | httpResource() | rxResource |
|---|---|---|---|
| Primary Use Case | General async operations with Promises | Simple HTTP GET requests | Working with existing RxJS Observables |
| Data Source | Any Promise-returning function | Built-in HttpClient (GET only) | RxJS Observable streams |
| Request Initiation | Lazy (when accessed) | Eager (immediate) | Lazy (when accessed) |
| Best For | Custom fetch logic, non-HTTP async operations | Simple data fetching from REST APIs | Integrating with existing RxJS services |
@angular/core/rxjs-interopAPIs that help you integrate RxJS & Signals.
Create a signal from an RxJs Observable with toSignal.
Create an RxJS Observable from a signal with toObservable.
Create an output based on an RxJs Observable.
Creating an RxJS Observable from a component or directive output.
Unsubscribing with takeUntilDestroyed
counterObservable = interval(1000);
// Get a Signal representing the counterObservable's value.
counter = toSignal(
this.counterObservable,
{initialValue: 0}
);export class SearchResults {
query: Signal<string> = inject(QueryService).query;
query$ = toObservable(this.query);
results$ = this.query$.pipe(
switchMap(query => this.http.get('/search?q=' + query ))
);
}const obs$ = toObservable(mySignal);
obs$.subscribe(value => console.log(value));
mySignal.set(1);
mySignal.set(2);
mySignal.set(3);@Directive({/*...*/})
class Draggable {
pointerMoves$: Observable<PointerMovements> = listenToPointerMoves();
// Whenever `pointerMoves$` emits, the `pointerMove` event fires.
pointerMove = outputFromObservable(this.pointerMoves$);
}@Component(/*...*/)
class CustomSlider {
valueChange = output<number>();
}
// Instance reference to CustomSlider.
const slider: CustomSlider = createSlider();
outputToObservable(slider.valueChange) // Observable<number>
.pipe(...)
.subscribe(...);export class UserProfile {
private dispatcher = inject(NotificationDispatcher);
private popup = inject(CustomPopupShower);
constructor() {
// This subscription the 'notifications' Observable is automatically
// unsubscribed when the 'UserProfile' component is destroyed.
const messages: Observable<string> = this.dispatcher.notifications;
messages.pipe(takeUntilDestroyed()).subscribe(message => {
this.popup.show(message);
});
}
}SoodAnkita
ankitasood.bsky.socialWebVibesOnlyGuacamoleAnkita