Rethinking reactivity in Angular
Andrei Antal
09.10.2025 Engineering Day @Raiffeisen Bank
Preamble
import { Component } from '@angular/core';
@Component({
selector: 'random',
templateUrl: './random.component.html',
})
export class RandomComponent {
// bla bla bla code
updateRandomData() {
...
// DO NOT REMOVE!!!
// for some reason angular won't detect changes, using setTimeout as a last resort, YOLO!
setTimeout(() => {
this.randomData = 'Updated random data';
}, 0);
}
}

// Complex search stream
const searchStream$ = input$.pipe(
switchMap(query =>
merge(refresh$, autoRefresh$).pipe(
startWith(null), // initial fetch
withLatestFrom(of(query)),
tap(([_, q]) => console.log('Preparing fetch for:', q)),
concatMap(([_, q]) =>
from(fetchData(q)).pipe(
retryWhen(errors =>
errors.pipe(
tap(err => console.warn('Retrying due to error:', err)),
delay(1000)
)
),
catchError(err => of(`Failed to fetch: ${err}`)),
map(result => ({ query: q, result })), // enrich data
tap(data => console.log('Fetched data object:', data)),
scan((acc, curr) => [...acc, curr.result], []), // accumulate results
tap(acc => console.log('Accumulated results:', acc)),
shareReplay(1), // replay last result for late subscribers
)
),
finalize(() => console.log(`Cleaning up stream for query: ${query}`))
)
),
exhaustMap(data => of(data).pipe(
tap(d => console.log('Passing data to final consumer:', d))
)),
takeUntil(interval(30000)), // auto-unsubscribe after 30s
);
import { of, combineLatest, concat } from 'rxjs';
import { delay, startWith } from 'rxjs/operators';
// Simulate loading states: true -> false
const loading$ = of(true).pipe(
concat(of(false).pipe(delay(1000)))
);
// Simulate a data fetch that takes 2s
const data$ = of(['Item A', 'Item B']).pipe(delay(2000), startWith([]));
// Combine latest
combineLatest([data$, loading$]).subscribe(([data, loading]) => {
console.log({ loading, data });
});
// { loading: true, data: [] }
// { loading: false, data: [] } ← glitch!
// { loading: false, data: ['Item A','Item B'] }

Andrei Antal
@andrei_antal
- web technologies & ux consultant
- trainer
- music and history lover
- community activist

organizer for ngBucharest
Hello!




History with Raiffesen
Rethinking Reactivity
How Angular’s shift toward fine-grained reactivity (with signals) changes the way we design, think, and code.

Reactivive change detection
State in web apps
const bookTitle = "Alchemy for beginners";
const chaptersTitles = [ "Equipment" ]
let book = {
title: bookTitle,
chapters: chapterTitles
}
.....

Strings, numbers, arrays, objects etc.
Document Object Model
(DOM)
(rendering)
User Interface = function(State)
(Data projection)
Reactive interfaces

Change sources in web apps
- User input (event callbacks)
- Async requests (xhr requests)
- Browser APIs - Promise, requestAnimationFrame
- Timers (setInterval, setTimeout)
Detecting changes in Angular
@Component({
selector: 'todo-item',
template: `
@for(let todo of todos; track $index) {
<div class="todo">
{{todo.owner.firstname}} - {{todo.description}} <br />
completed: {{todo.completed}}
</div>
<button (click)="completeTodo(todo.id)">COMPLETE</button>
}`
})
class TodosComponent implements OnInit{
#http = inject(Http)
todos:Todo[] = [];
ngOnInit() {
this.#http.get('/todos')
.subscribe(todos => this.todos = todos);
setTimeout(() => {/*some other changes*/})
}
completeTodo(id: number) { ... }
}
function addEventListener(eventName, callback) {
// call the real addEventListener
callRealAddEventListener(eventName, function() {
// first call the original callback
callback(...);
// and then run Angular-specific functionality
var changed = angular2.runChangeDetection();
if (changed) {
angular2.reRenderUIPart();
}
});
}
Zone.js
ApplicationRef


Detecting changes
"Dirty" marking
OnPush
import { ChangeDetectionStrategy } from '@angular/core';
@Component({
...
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
@Input() data;
}
@Component({
template: `
<my-counter [data]="data" />
<button (click)="count()">COUNT</button>
`
})
export class App {
data = { counter: 0 };
count() {
// won't trigger change detection
this.data.counter++;
// will trigger change detection
this.data = { counter: this.data.counter + 1 };
}
}
OnPush
Why mark ancestors?
RxJS
- A stream of data ( 0 or more values of any type)
- Pushed over any amount of time (can end, but not necessarily)
- Cancelable ( can be stopped from emitting a value - unsubscribed)
- Lazy - won't emit values until we subscribe to them

OBSERVABLES
Async pipe
Triggering CD
todos$ = this.#http.get('/todos') // uses XMLHttpRequest
$todos = new behaviourSubject(null)
addTodo(todo) { // click -> addEventListener
this.$todos.next([...this.$todos.value, todo])
}
ngOnInit() {
setTimeout() { // timer API
this.$todos.next([...])
}
}
Triggering CD
Marking dirty

Marking dirty
Angular’s reactivity used to be about streams
Reactivity ≠ Observables; it’s about state and dependencies
Signals
the new reactive primitive in Angular
Signals and Observables
import { signal, computed, effect } from '@angular/core';
const price = signal(100);
const quantity = signal(2);
const total = computed(() => price() * quantity());
effect(() => {
console.log(`Total cost: $${total()}`);
});
price.set(120); // Logs: "Total cost: $240"
quantity.update(q => q + 1); // Logs: "Total cost: $360"
const subject = new Subject<number>();
subject.subscribe(console.log);
subject.next(1);
subject.next(2);
subject.next(3);
console.log('logging here')
const mySignal = signal(0)
effect(() => console.log(mySignal()))
mySignal.set(1)
mySignal.set(2)
mySignal.set(3)
console.log('logging here')
1
2
3
logged here
logged here
3
It’s not just a new API — it’s a new "way of thinking"
Signals and Observables
SIGNALS | OBSERVABLES |
---|---|
one value | many values, over time |
always Sync | mainly async but could be sync |
imperative API | declarative API |
explicit producer | implicit producer |
no subscription | subscription need |
auto memory management | need to manually unsubsc |
Signal Change Detection
Change detection wth Signals
Templates are now seen as effects
Road to fine grained CD
Fine grained reactivity
Zoneless today
OnPush + async pipe (or manual markForCheck)
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(),
provideBrowserGlobalErrorListeners()
]
});
*developer preview in v20
+ remove zone.js from polyfills (~19kb)
You don’t have to abandon RxJS — just use it where it makes sense.
New Signal APIs
New signal APIs
- signal input/output
- view & content queries
- model
- linkedSignal
- resource & httpResource
- routerOutletData & currentNavigation
- signal forms 🧪
effect(() => {
// Get the current navigation
const navigation = this.router.getCurrentNavigation();
if (navigation) {
console.log('Current navigation:', navigation);
console.log('Navigation extras state:', navigation.extras.state);
} else {
console.log('No current navigation (maybe page was refreshed).');
}
})
count = input<number>
disabled = input<boolean, unknown>(false, { transform: booleanAttribute });
// @Input() count: number
// @Input({ transform: booleanAttribute }) disabled
double = computed(() => {
const disabled = this.disabled();
const double = this.count() * 2
return disabled ? 0 : double
})
// ngOnChanges ...
maxReached = output<void>()
//@Output maxReached = new EventEmitter()
maxReached.emit()
@Component({
selector: 'custom-checkbox',
template: '<div (click)="toggle()"> ... </div>',
})
export class CustomCheckbox {
checked = model(false);
toggle() {
this.checked.set(!this.checked());
}
}
@Component({
template: '<custom-checkbox [(checked)]="isAdmin" />',
})
export class UserProfile {
isAdmin = signal(false);
}
export class CounterComponent {
// Base signal
count = input(0);
// Computed signal derived from count
doubleCount = computed(() => this.count() * 2);
// Linked signal: automatically mirrors the base signal
doubleCountLinked = linkedSignal(() => this.count() * 2);
increment() {
// X - computeds are read-only
this.doubleCount.set(() => (this.doubleCount() + 1) * 2)
// Update linkedCount, automatically updates count too
this.linkedCount.update(n => n + 1);
}
decrement() {
this.linkedCount.update(n => n - 1);
}
}
todoId = signal<number | null>(null);
// RESOURCE
singleTodo = resource({
request: () => ({ id: this.todoId() }), // trigger for request
loader: ({ request, abortSignal }) => {
if (request.id) {
return fetch(
`https://jsonplaceholder.typicode.com/todos/${request.id}`,
{
signal: abortSignal,
}
).then((response) => response.json());
}
return Promise.resolve({});
},
});
// HTTP RESOURCE
singleTodo = httpResource(
() => `https://jsonplaceholder.typicode.com/todos/${this.todoId()}`, // trigger
{
map: (res: Todo): any => `<h1>${res.title}</h1>`,
defaultValue: null,
}
);

...about AI...
One last thing...
Thank you!

<= slides
Rethinking Reactivity
By Andrei Antal
Rethinking Reactivity
- 15