Skip to main content

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();
}