Angular Signals
Redux
RxJs
MobX
NgRx
MobX
SolidJS
MobX = Proxy + Signals
import { observable, computed, action, flow } from "mobx"
class Doubler {
@observable accessor value
constructor(value) {
this.value = value
}
@computed
get double() {
return this.value * 2
}
@action
increment() {
this.value++
}
@flow
*fetch() {
const response = yield fetch("/api/value")
this.value = response.json()
}
}

autorun(() => {
console.log(doubler.double);
})
SolidJS
import { createSignal, onCleanup } from "solid-js";
import { render } from "solid-js/web";
const CountingComponent = (props) => {
const [count, setCount] = createSignal(0);
const interval = setInterval(
() => setCount(c => c + 1),
1000
);
onCleanup(() => clearInterval(interval));
return (
<div>
<h2>{props.appName}</h2>
<div>Count value is {count()}</div>
</div>
);
};
render(() => <CountingComponent appName={"Counter APP"} />, document.getElementById("app"));
granular updates!
Примитивы - signal
import { signal } from '@angular/core';
const count = signal(0);
console.log('The count is: ' + count()); // The count is: 0
count.set(1); /* or */ count.update(v => v++);
console.log('The count is: ' + count()); // The count is: 1
Примитивы - computed
import { signal, computed } from '@angular/core';
const count = signal(0); // WritableSignal<number>
const showCount = signal(false); // WritableSignal<boolean>
const countDisplay = computed(() => { // Signal<string>
if (showCount()) {
return `The count is ${count()}`;
} else {
return 'Counter is hidden';
}
});
console.log(countDisplay()); // Counter is hidden
showCount.set(true);
console.log(countDisplay()); // The count is 0
Примитивы - effect
import { signal, computed, effect } from '@angular/core';
const count = signal(0);
const showCount = signal(false);
const countDisplay = computed(() => {
if (showCount()) {
return `The count is: ${count()}`;
} else {
return 'Counter is hidden';
}
});
effect(() => {
console.log(countDisplay());
}, { injector, manualCleanup: true, allowSignalWrites: false });
showCount.set(true); // The count is: 0

Примитивы - effect
import { signal, computed, effect } from '@angular/core';
const count = signal(0);
const showCount = signal(false);
const countDisplay = computed(() => {
if (showCount()) {
return `The count is: ${count()}`;
} else {
return 'Counter is hidden';
}
});
const effectRef = effect(() => {
console.log(countDisplay());
}, { injector, manualCleanup: true, allowSignalWrites: true });
showCount.set(true);
showCount.set(false);
showCount.set(true);
effectRef.destroy(); // ?
Как работает
const count = signal(0);
const showCount = signal(false);
const countDisplay = computed(() => {
if (showCount()) {
return `The count is: ${count()}`;
} else {
return 'Counter is hidden';
}
});
effect(() => {
console.log(countDisplay());
});
queueMicrotask(() => showCount.set(true));
Reactive Graph
Watch Node
effect
activeConsumer
Как работает
Reactive Graph
Computed Node
countDisplay
Watch Node
effect
activeConsumer
countDisplay.consumers = [effect]
effect.producers= [countDisplay]
const count = signal(0);
const showCount = signal(false);
const countDisplay = computed(() => {
if (showCount()) {
return `The count is: ${count()}`;
} else {
return 'Counter is hidden';
}
});
effect(() => {
console.log(countDisplay());
});
queueMicrotask(() => showCount.set(true));
Как работает
Reactive Graph
Signal Node
showCount
Computed Node
countDisplay
Watch Node
effect
activeConsumer
countDisplay.producers= [count, showCount]
showCount.consumers = [countDisplay]
const count = signal(0);
const showCount = signal(false);
const countDisplay = computed(() => {
if (showCount()) {
return `The count is: ${count()}`;
} else {
return 'Counter is hidden';
}
});
effect(() => {
console.log(countDisplay());
});
queueMicrotask(() => showCount.set(true));
Как работает
Reactive Graph
Signal Node
showCount
Computed Node
countDisplay
Watch Node
effect
const count = signal(0);
const showCount = signal(false);
const countDisplay = computed(() => {
if (showCount()) {
return `The count is: ${count()}`;
} else {
return 'Counter is hidden';
}
});
effect(() => {
console.log(countDisplay());
});
queueMicrotask(() => showCount.set(true));
// Counter is hidden
Как работает
Reactive Graph
Signal Node
showCount
Computed Node
countDisplay
Watch Node
effect
const count = signal(0);
const showCount = signal(false);
const countDisplay = computed(() => {
if (showCount()) {
return `The count is: ${count()}`;
} else {
return 'Counter is hidden';
}
});
effect(() => {
console.log(countDisplay());
});
queueMicrotask(() => showCount.set(true));
showCount.consumers.forEach((c) => c.update())
live
live
countDisplay.producersStates.showCount.version ++
countDisplay.producers = []
countDisplay.run()
Как работает
Reactive Graph
Signal Node
showCount
Computed Node
countDisplay
Watch Node
effect
const count = signal(0);
const showCount = signal(false);
const countDisplay = computed(() => {
if (showCount()) {
return `The count is: ${count()}`;
} else {
return 'Counter is hidden';
}
});
effect(() => {
console.log(countDisplay());
});
queueMicrotask(() => showCount.set(true));
Signal Node
count
countDisplay.consumers.forEach((c) => c.update())
effect.producersStates.countDisplay.version ++
effect.run()
Как работает
Reactive Graph
Signal Node
showCount
Computed Node
countDisplay
Watch Node
effect
const count = signal(0);
const showCount = signal(false);
const countDisplay = computed(() => {
if (showCount()) {
return `The count is: ${count()}`;
} else {
return 'Counter is hidden';
}
});
effect(() => {
console.log(countDisplay());
});
queueMicrotask(() => showCount.set(true));
Signal Node
count
// The count is 0
Angular -> signals
import {Component, input, model, ChangeDetectionStrategy} from '@angular/core'; @Component({ selector: '...', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <p>First name: {{firstName()}}</p> <p>Last name: {{lastName()}}</p> ` }) export class MyComp { // optional firstName = input<string>(); // InputSignal<string|undefined> // required lastName = input.required<string>(); // InputSignal<string> // alias age = input(0, {alias: 'studentAge'}); // InputSignal<number> //transform disabled = input(false, { // InputSignalWithTransform<boolean, string | boolean> transform: (value: boolean|string) => typeof value === 'string' ? value === '' : value, }); checked = model(false); // ModelSignal<boolean> Two-way binding! toggle() { this.checked.set(!this.checked()); } }
RxJs -> signals
const count$ = interval(1000); const count = toSignal(count$, { initialValue, injector, manualCleanup, rejectErrors, requireSync }); const count2$ = toObservable(count, { injector });

Рефакторинг
@LazyGetter() get size$(): Observable<Size> { return combineLatest([this.grid.zoomed$, this.grid.ejected$]).pipe( switchMap(([zoomed, ejected]) => { if (zoomed || ejected) return of('full' as Size); return fromResize(this.el.nativeElement).pipe( map((size): Size => { if (Math.min(size.contentRect.width, size.contentRect.height) < 308) { return 'compact'; } else { return 'collapsed'; } }), ); }), distinctUntilChanged(), shareLastValue(), ); }
Было:
resizeInfo = toSignal(fromResize(this.el.nativeElement)); size = computed<Size>(() => { if (this.grid().zoomed() || this.grid().ejected()) return 'full'; const resizeInfo = this.resizeInfo(); if (resizeInfo && Math.min(resizeInfo.contentRect.width, resizeInfo.contentRect.height) < 308) { return 'compact'; } else { return 'collapsed'; } });
Стало:
Рефакторинг
@LazyGetter() get whiteboardName$() { return this.app$.pipe(switchMap((app) => app!.title$)); }
@LazyGetter() get whiteboardLink$() { return this.app$.pipe( switchMap((app) => app?.linkData$ ?? of(undefined)), distinctUntilChanged(shallowEqual), ); }
Было:
whiteboardName = computed(() => this.app()?.title()); whiteboardLink = computed(() => this.app()?.linkData());
Стало:
Рефакторинг
private _linkData$ = new BehaviorSubject<WhiteboardLink | undefined>(undefined); get linkData$(): Observable<WhiteboardLink | undefined> { return this._linkData$.pipe(distinctUntilChanged(shallowEqual)); } get linkData(): WhiteboardLink | undefined { return this._linkData$.value; }
private _linkData = signal<WhiteboardLink | undefined>(undefined); get linkData() { return this._linkData.asReadonly(); }
Было:
Стало:
Рефакторинг
Было:
@if ((size$ | async) === 'full') { @if (whiteboardName$ | async; as whiteboardName ) { <header> ... </header> } @if (whiteboardLink$ | async) { <dynamic-component ... /> } }
@if (size() === 'full') { @if (whiteboardName()) { <header> ... </header> } @if (whiteboardLink()) { <dynamic-component ... /> } }
Стало:
Рефакторинг
public buildAppGridApi(app: ConferenceApp): AppGridApi { const zoomed$ = this.zoomedGridElement$.pipe(map((zoomed) => zoomed?.id === app.id)); return { zoomed$, ejected$: this.ejectedGridElement$.pipe(map((ejected) => ejected?.id === app.id)), fullscreen$: combineLatest([zoomed$, this.viewFullscreenMode$]).pipe(map((values) => values.every(Boolean))), ... };
Было:
public buildAppGridApi(app: ConferenceApp): AppGridApi { const zoomed = toSignal(this.zoomedGridElement$, { injector: this.injector }); const ejectedGridElement = toSignal(this.ejectedGridElement$, { injector: this.injector }); const viewFullscreenMode = toSignal(this.viewFullscreenMode$, { injector: this.injector }); return { zoomed: computed(() => zoomed()?.id === app.id), ejected: computed(() => ejectedGridElement()?.id === app.id), fullscreen: computed(() => zoomed() && viewFullscreenMode()), ... }; }
Стало:
Рефакторинг
Стало:
@LazyGetter() get ejectedGridElement() { return toSignal(this.ejectedGridElement$, { injector: this.injector }); } @LazyGetter() get zoomedGridElement() { return toSignal(this.zoomedGridElement$, { injector: this.injector }); } @LazyGetter() get viewFullscreenMode() { return toSignal(this.viewFullscreenMode$, { injector: this.injector }); } public buildAppGridApi(app: ConferenceApp): AppGridApi { const zoomed = computed(() => this.zoomedGridElement()?.id === app.id); return { zoomed, ejected: computed(() => this.ejectedGridElement()?.id === app.id), fullscreen: computed(() => zoomed() && this.viewFullscreenMode()), ... }; }
Надо договорится
- Нейминг сигналов?
- Сигналы - для синхронно-реактивного кода. Для асинхронного лучший вариант это rxJs
- effect надо осторожно, для чего-то очень простого
// bad
effect(() => {
this.menuOpened(); // подписка на app
this.loading.set(true);
});
// ok
toObservable(this.menuOpened)
.pipe(takeUntilDestroyed()) // вроде не обязательно
.subscribe(() => {
this.loading.set(true);
});
Signals
By Andrey Osipov
Signals
- 197