Reactivity In Actions
Published: 2022-09-06 14:29:00

In [Angular State Library](https://github.com/antischematic/angular-state-library) actions are everything. And everything is an action.

> Disclaimer: This project is an experiment, it is not production ready.

```tsx
@Store()
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [Counter.Provide(UICounter)],
  selector: 'ui-counter',
  template:
    <p>UICounter: {{ count }}</p>
    <dd>
      <ng-content></ng-content>
    </dd>
})
export class UICounter {
  @Input() count = 0;
  @Output() countChange = new EventEmitter<number>(true);

  @Invoke() autoIncrement() {
    this.count++;
    this.countChange.emit(this.count);

    return dispatch(timer(1000), {
      next: this.autoIncrement,
    });
  }
}
```

The secret to reactivity in Angular is knowing when to run change detection. By default, Angular runs change detection for us automatically every time we interact with the DOM or do some async work. This magical reactivity is what sets Angular apart from the crowd. But it comes at a cost. Performance.

## Pushing Problems

As an Angular application grows in size and complexity, we start running into problems. The more components we render, the slower each render cycle gets. This is because Angular dirty checks every data binding in every component every time change detection runs. How often does change detection run? A lot.

The root cause of this slow down is Zone.js. On every user interaction, microtask (eg. promises) and macrotask (eg. timers), Zone.js lets Angular know that it should run change detection in case some data binding in the app has changed. We have a budget to keep of 16ms (60Hz) or 8ms (120Hz) so that the user experience remains fluid. Angular's change detection cycle can easily exceed this limit.

To manage this problem, Angular says we should configure components to use the `OnPush` change detection strategy.

{% embed https://stackblitz.com/edit/angular-ivy-edyckq?ctl=1&embed=1&file=src%2Fapp%2Fui-counter.ts,src%2Fapp%2Fui-descendent.ts,src%2Fapp%2Fui-double.ts,src%2Fapp%2Fhello.component.ts&hideExplorer=1&hideNavigation=1 %}
<center><small>The OnPush Problem</small></center><br/><br/>

The `OnPush` strategy tells Angular that this component's change detection tree should only be checked:

1. When `@Input` bindings change.

2. When an `(event)` binding emits.

3. When change detection is run manually.

In the example above we can see that the components rendered inside the `OnPush` component do not update when the counter changes. Since there is no input binding and no template event, that leaves only one option.

## Manual Change Detection

At some point we will need to call `markForCheck`. This marks the view dirty so that Angular will check the component and update its host bindings and template bindings if necessary. There are two common approaches:

### Async Pipes

Most Angular apps use `AsyncPipe`, which calls `markForCheck` internally. This in my opinion creates more problems than it solves, due to the cognitive load of async behaviour in templates plus the non-intuitive nature of RxJS. If a developer isn't familiar with the differences between unicast/multicast, and hot/cold observables, they're going to have a bad time.

### Manual Subscribe

Subscribing in components is often frowned-upon, but it's a lot easier than using `AsyncPipe`, especially for callbacks.

```ts
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template:
    <div>{{ data | json }}</div>
    <button (click)="loadData()">
      Load Data
    </button>
})i
export class UIData {
  private http = inject(HttpClient)
  private changeDetector = inject(ChangeDetectorRef)

  data

  loadData() {
    this.http.get(endpoint).subscribe((data) => {
      this.data = data
      this.changeDetector.markForCheck()
    })
  }
}
```

<center><small>What about subscription management?</small></center><br/>

Of course, we still need to know when and how to manage long-lived subscriptions to prevent memory leaks.

## Square Peg, Round Hole

Both approaches to manual change detection are lacking. This is because RxJS and Angular are built on fundamentally different paradigms. RxJS is functional and composable, while Angular components are object-oriented and imperative. 

![Square peg round hole](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zjrdhq9gevdaz8aj7q1p.png)

<center><small>Putting RxJS into Angular templates</small></center><br/>

There are many existing solutions to this problem, but all come at the cost of increased complexity. We can offload state to services, but now we need a way to connect the service to our component. We can offload state to redux, but now we have to manage several layers of indirection. Why can't state management be simpler?

## `Observable` Core, `@Directive` Shell

<small>Hat tip [Destroy All Software](https://www.destroyallsoftware.com/talks/boundaries)</small>

Angular State Library solves the `OnPush` problem, combining Vue-style reactivity, RxJS and Redux. It also solves the square peg/round hole problem by keeping state management imperative on Angular's side while extracting RxJS into functional-composable side effects. Effects are then mapped back onto the component's state using imperative callbacks (ie. action reducers).

Any directive can be converted into a redux store without added layers of indirection and boilerplate. The possibilities are endless.

{% embed https://stackblitz.com/edit/angular-ivy-ub3pso?ctl=1&embed=1&file=src%2Fapp%2Fui-counter.ts,src%2Fapp%2Fui-descendent.ts,src%2Fapp%2Fui-double.ts,src%2Fapp%2Fproviders.ts,src%2Fapp%2Fhello.component.ts&hideExplorer=1&hideNavigation=1 %}

<center><small>Reactivity in Action</small></center><br/><br/>

The secret to reactivity is observable events. All events generated by Angular State Library are dispatched to a global event stream. All events are generated by actions.

1. The event stream is populated by calls to `@Action` methods
2. `@Input` changes are also tracked as events via `ngOnChanges`.
3. The event stream captures the `next`, `error` and `complete` events from dispatched effects.
4. Components connect to a store by providing a `StoreToken`.
5. The `StoreToken` subscribes to the store's event stream and triggers `markForCheck` to update the view when an event is emitted. 
6. The `StoreToken` returns the store's state as a reactive proxy object for tracking in other actions and selectors.

Now we can inject stores anywhere without worrying about `OnPush` or subscriptions. It just works.

## Help Wanted

This project is currently a proof of concept. It's no where near production ready. If you are interested in contributing or dogfooding feel free to [open a line on Github discussions](https://github.com/antischematic/angular-state-library/discussions/1), or leave a comment with your thoughts below.

Thanks for Reading!