Angular State Library
Published: 2022-09-03 15:32:44

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

## Introduction

**Why:** I love RxJS. The integration between Angular and RxJS leaves much to the imagination. Existing state libraries are complicated and verbose.

**How:** It uses TypeScript decorators to wrap class methods in an injection context. ES6 Proxies wrap `this` to capture reactive dependencies. Reactive dependencies are dirty-checked during change detection. Reactive functions are invoked when their dependencies have changed.

**What:** Angular State Library borrows from Vue's reactivity system, combining it with RxJS observables and Redux, without boilerplate.

## Redux: The Good Parts

Redux is a simple idea. State plus action equals new state. Also known as a reducer. For some this is means pure functions. But libraries like Immer and redux-thunk allow us to describe state changes in other, more ergonomic ways.

```ts
@Component()
export class UICounter {
   count = 0

   increment() {
      this.count++
   }
}
```

<center><small>Is this redux?</small></center><br/>

Another aspect of redux is observability. One part of the system can observe the outputs of another part of the system and perform more actions. Sometimes we call these sagas, or side effects. Action plus side effect equals more actions.

```ts
@Component()
export class UICounter {
   count = 0

   increment() {
      this.count++

      setTimeout(() => {
         this.count *= 2
      }, 2000)
   }
}
```

<center><small>Is this redux?</small></center><br/>

There are other benefits to using redux, such as immutability and development tools. These are secondary concerns. What we have done is boil down redux to two simple principles.

1. State is mutated when an action occurs.
2. Side effects are triggered after an action occurs.

We can implement this however we want. For Angular, we can do a whole lot more.

## Angular State Library 

```ts
@Store()
@Component()
export class UICounter {
   count = 0

   @Action() increment() {
      this.count++

      return dispatch(timer(2000), () =>
         this.count *= 2
      })
   }
}

const dispatch = createDispatch(UICounter)
```

<center><small>Angular State Library is Redux reimagined.</small></center><br/>

The goal of this project is to eliminate the complexity of state management in Angular. There are no action factories here. No modules, no facades, no adapters. Just a simple Angular directive. Add some decorators and we're good to go.

## Declarative State

State changes can be expressed declaratively. Functions decorated by `@Invoke()` are called automatically during the first change detection run, and each time its dependencies change there after.

```ts
@Store()
@Component()
export class UICounter {
   @Input() count = 0

   increment() {
      this.count++
   }

   @Invoke() logCount() {
      console.log(this.count)
   }
}
```

<center><small>[See it in action on StackBlitz](https://stackblitz.com/edit/angular-ivy-tm5z3d?file=src%2Fapp%2Fcounter.component.ts)</small></center><br/>

Here we see `logCount` is a reactive function that logs the current value of `count` whenever it changes. If you're familiar with Vue's reactivity system it's kind of like `watchEffect`. But there are some big differences:

1. No compiler tricks
2. No new reactive primitives (like `ref`)
3. Shallow proxies by default

This works because we are able to replace `this` with an ES6 Proxy object that marks property access on `this` as a dependency. When Angular's change detection cycle runs it checks the dependencies and invokes the action each time they change. It doesn't work on arrow functions.

More importantly, `Invoke` is just an alias for `Action`. We can observe its calls through the event stream.

## Selectors

Think computed properties. Angular State Library has two types of selectors:

**Field selectors**

```ts
@Store()
@Component()
export class UICounter {
   @Input() count = 0

   @Select() get double() {
      return this.count * 2
   }
}
```

Field selectors wrap the `this` object in a shallow proxy that tracks dependencies, and only recalculates the value when those dependencies change. It's also lazy; nothing happens until we read the value.

**Parameterized selectors**

```ts
@Store()
@Component()
export class UICounter {
   @Input() todos = []

   @Select() getTodosWith(text) {
      return this.todos.filter(todo => todo.title.match(text))
   }
}
```

Parameterized selectors are the same as field selectors except the function arguments are also memoized. By default all arguments are memoized without limit, and reset when the object dependencies change. For example, if I query `getTodosWith` with the values "Bob", "Jane" and "George", all three queries would be memoized until the `todos` array changes, which clears the cache.

## Side Effects

In Angular State Library, side effects are just plain RxJS observables. Effects are dispatched by actions, and each action can dispatch just one effect.

```ts
@Store()
@Component()
export class UICounter {
   count = 0

   @Action() increment() {
      this.count++

      return dispatch(timer(2000), () =>
         this.count *= 2
      })
   }
}
```

### `dispatch`

First let's look at `dispatch`. It is used to dispatch effects and can only be used inside an action stack frame (ie. a method decorated with `Action` or one of its derivatives).

The dispatcher accepts two arguments: an observable source and an (optional) observer that receives its events. The dispatcher returns an observable that mirrors the original observable for further chaining.

When an action returns an observable the subscription doesn't happen immediately. The store waits until all actions have executed before it subscribes to effects.

### `useOperator`

By default, the previous effect is cancelled each time an action dispatches a new effect. This is consistent with other reactivity systems. But RxJS lets us do more.

`useOperator` is used to configure the switching behaviour of effects. For example, if we want to merge effects instead of switching them:

```ts
@Store()
@Component()
export class UICounter {
   count = 0

   @Action() increment() {
      this.count++

      useOperator(mergeAll())

      return dispatch(timer(2000), () => {
         this.count *= 2
      })
   }
}
```

<center><small>[See it in action on StackBlitz](https://stackblitz.com/edit/angular-ivy-expfty?file=src%2Fapp%2Fcounter.component.ts)</small></center><br/>

Try clicking the button many times and watch what happens. Now swap `mergeAll()` with `switchAll()` and click a few more times. See the difference? RxJS gives us amazing temporal powers.

> Note: The flattening operator passed to `useOperator` cannot be changed once it has been set. It is locked in the first time it is called.

`useOperator` can also be composed with effects.

```ts
function updateTodo(todo: Todo): Observable<Todo> {
   useOperator(mergeAll())
   return inject(HttpClient).put<Todo>(
      `https://jsonplaceholder.typicode.com/todos/${todo.id}`,
      todo
   );
}
```

```ts
function toggleAll(todos: Todo[]): Observable<Todo[]> {
   useOperator(exhaustAll())
   return forkJoin(
      todos.map(todo => updateTodo({ 
         ...todo, 
         completed: !todo.completed
      }))
   );
}
```

## Sagas

For most cases the one-to-one relationship between actions and effects should suffice, especially for basic I/O operations like fetch. But when that's not enough, we can observe the full stream of events to create sagas.

```ts
@Store()
@Component()
export class UICounter {
   count = 0

   @Action() increment() {
      this.count++
   }

   @Action() double() {
      this.count *= 2
   }

   @Invoke() doubleAfterIncrement() {
      const effect = fromStore(UICounter).pipe(
         filter(event => event.name === "increment"),
         filter(event => event.type === "dispatch"),
         delay(2000)
      )

      return dispatch(effect, {
         next: this.double
      })
   }
}
```

The `fromStore` helper can be used to observe all events, including effects, with full type inference.


## Putting it all Together

For a full example showcasing most of the features in Angular State Library (and some bonus features like lazy loading and transitions) [view the example app on StackBlitz](https://stackblitz.com/edit/angular-state-library?file=src%2Fapp%2Fui-todos.component.ts).

## Help wanted

This project is just 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!