Logic comparison - Increment a Value
This snippet compares 3 different implementations of the same problem. It serves as a small refactoring guide and shows the difference of imperative and declarative/reactive programming.
This snippet uses the rxLet
directive as replacement for Angular's async
pipe.
All examples will work with the async
.
Problem: We have a component that:
- maintains a state
{ count: number }
- displays the actual value of
count
- increments the count over a button click binding
Imperative
State:
The component's state is a simple object state: { count: number } = { count: 0 };
.
Display:
To display the value we use a template expression {{ state.count }}
.
This expression gets reevaluated whenever the component re-renders.
Action:
The state gets incremented by one whenever the button gets clicked.
The click binding is set-up over an event binding (click)
and fires the callback onClick
.
This callback increments the state's count
property, this.state.count = this.state.count + 1;
Rendering:
The click binding gets detected by zone which in turn flags this component and all of its ancestors as dirty.
This results in an ApplicationRef.tick
call which re-renders all dirty flagged components.
@Component({
selector: 'my-comp',
template: `
<div>Value: {{ state.count }}</div>
<button (click)="onClick($event)">Increment</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {
state: { count: number } = { count: 0 };
onClick(e) {
this.state.count = this.state.count + 1;
}
}
Reactive reading
State:
The component's state gets managed with rxState
function.
The component's state is a simple interface: { count: number }
.
Inside the class we expose our state as Observable private readonly state$ = this.state.select();
Display:
To display the value we use a simple structural directive called *rxLet
which binds the state$
property of the component to its host element
. We can then assign our state observable to a local template variable
.
Whenever the bound Observable emits a new value the rxLet
directive flags this component and all of its ancestors as dirty.
Action:
The state gets incremented by one whenever the button gets clicked.
The click binding is set-up over an event binding (click)
and fires the callback onClick
.
This callback increments the state's count
property by sending the new value this.state.set('count', s => s.count + 1);
Rendering:
The click binding gets detected by zone which in turn flags this component and all of its ancestors as dirty.
This results in an ApplicationRef.tick
call which re-renders all dirty flagged components.
@Component({
selector: 'my-comp',
template: `
<div *rxLet="state$; let s">Value: {{ s.count }}</div>
<button (click)="onClick($event)">Increment</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {
private readonly state = rxState<{ count: number }>(({ set }) =>
set({ count: 0 })
);
readonly state$ = this.state.select();
onClick(e) {
this.state.set('count', (state) => state.count + 1);
}
}
Reactive Writing
State:
The component's state gets managed with rxState
function.
The components state is a simple interface { count: number }
.
Inside the class we expose our state as Observable readonly state$ = this.state.select();
Display:
To display the value we use a a simple structural directive called *rxLet
which binds the state$
property of the component to its host element
. We can then assign our state observable to a local template variable
.
Whenever the bound Observable emits a new value the rxLet
directive flags this component and all of its ancestors as dirty.
Action:
The state gets incremented by one whenever the button gets clicked.
In the class we use a Subject to track clicks readonly increment$ = new Subject<void>();
.
The click binding is set-up over an event binding (click)
and fires the Subjects next
method.
This Observable gets connected to the component's state in the setup function connect(this.increment$, (state) => ({ count: state.count + 1 }))
.
Whenever the Subject emits, we apply the increment logic passed as a function.
Rendering:
The click binding gets detected by zone which in turn flags this component and all of its ancestors as dirty.
This results in an ApplicationRef.tick
call which re-renders all dirty flagged components.
@Component({
selector: 'my-comp',
template: `
<div *rxLet="state$; let s">Value: {{ s.count }}</div>
<button (click)="increment$.next()">Increment</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {
readonly increment$ = new Subject<void>();
private readonly state = rxState<{ count: number }>(({ set, connect }) => {
set({ count: 0 });
connect(this.increment$, (state) => ({ count: state.count + 1 }));
});
readonly state$ = this.state.select();
}
Control rendering with RxUnpatch
In this section we use the unpatch
directive to get control over rendering.
The sections State and Action are identical.
The Display has a small difference. We use the unpatch
directive to get rid of renderings caused by the button eventListener.
Rendering:
A rerender gets only triggered by the rxLet
directive. The process is the same as before.
@Component({
selector: 'my-comp',
template: `
<div *rxLet="state$; let s">Value: {{ s.count }}</div>
<button [unpatch] (click)="increment$.next()">Increment</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {
readonly increment$ = new Subject<void>();
private readonly state = rxState<{ count: number }>(({ set, connect }) => {
set({ count: 0 });
connect(this.increment$, (state) => ({ count: state.count + 1 }));
});
readonly state$ = this.state.select();
}
Control rendering direction with rendering strategies
In this section we use the strategy
option of the rxLet
directive to get advanced control over rendering.
The sections State and Action are identical to the previous examples.
Rendering:
The rendering still gets managed by the rxLet
Directive. But with the strategy
set to local
changes will not result in a re-render of any ancestor component. Thus saving you tons of rendering cycles.
@Component({
selector: 'my-comp',
template: `
<div *rxLet="state$; let s; strategy: 'local'">Value: {{ s.count }}</div>
<button (click)="increment$.next()">Increment</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {
readonly increment$ = new Subject<void>();
private readonly state = rxState<{ count: number }>(({ set, connect }) => {
set({ count: 0 });
connect(this.increment$, (state) => ({ count: state.count + 1 }));
});
readonly state$ = this.state.select();
}