Angular Signals & Observables: Differences
Tags: angular, signals, observables, rxjs
Published: 2023-07-15 10:50:00

Angular 16 brought a new (for Angular) reactivity primitive, and Angular Signals will be, unavoidably, compared to Observables. I'll highlight their differences.

## Signals always have a value

Observables can emit their values synchronously or asynchronously. 
Signals don't emit anything, a consumer should "pull" their value when needed, and some value will always be (synchronously) returned. It's an important difference and I'll explain it using RxJS operators and Angular Signals functions.

> To variables containing Signals, I'll add `$` as a prefix: `$foo`.
> To variables containing Observables, I'll add `$` as a suffix: `foo$`.

## `combineLatest()`

To simplify things, we could say that

```ts
const $v = computed(() => $foo() * $bar());
```

is the same as:

```ts
const v$ = combineLatest([foo$, bar$]).pipe(
    map(([foo, bar]) => foo * bar)
);
```

But, this simplification is not quite correct.
`combineLatest()` and `combineLatestWith()` will not emit anything, until every given observable will emit at least one value. Signals have no such limitation and you don't need to worry about it.

We should not forget that every observable passed to `combineLatest[With]()` should emit a value **_after_** we subscribed (or should have a buffered value). Signals have no such limitation.

If one of the observables passed to `combineLatest()` will complete **before** emitting a value, the resulting observable will instantly complete and will not emit anything. Signals don't have a "complete" state, so this limitation also doesn't apply to signals.

If at least one of the observables errors, `combineLatest[With]()` will also error and - which is quite important - it will unsubscribe immediately. Signals will just return an error on every read until a non-error value can be produced. They will not unsubscribe, because they should always have a value.

Also, if in the resulted observable you'll modify one of the observables, observed by `comineLatest()` or `combineLatestWith()`, you'll create an infinite loop. In `computed()` and `effect()` writing to signals is forbidden by default. There are ways to bypass it, but at least you'll be warned and, most of the time, you'll avoid an infinite loop.

Another difference is caused by the fact that Signals don't emit their values, they will notify the consumer, but the consumer should decide when the value should be pulled. In case of `computed()` it's the moment we read the value of the computed signal, in case of `effect()` it will happen in the next microtask if at least one notification was received.

Because of that, `combineLatest[With]()` will emit a value every time when any of the given observables emits a value, but `computed()` will not emit anything at all, and `effect()` will emit just one notification, even if multiple signals inside the `effect()` will emit notifications.

This difference makes signals "glitch-free", and it can, in fact, remove some pointless execution cycles. But, sometimes such behavior is undesired, and when we need to receive an update from every emitter and run an execution instantly - in such case we need an observable. 

## `withLatestFrom()`
Almost all the differences, listed for combineLatest(), are applicable to `withLatestFrom()`.

So, this comparison is also not quite correct:

```ts
const $v = computed(() => $foo * untracked($bar));
```

and

```ts
const v$ = combineLatest([foo$]).pipe(
  withLatestFrom(bar$),
  map(([[foo], bar]) => foo * bar)
);
```

`computed()` will not be subscribed to `$bar`, but the resulting signal will not wait for a value from `$bar`, because signals always have a value, and an error from `$bar` will not cause termination of the resulting signal (but it will keep returning an error until `$bar` is "fixed").

One difference that is not applicable: `untracked()` lets us write to the signals, so we can cause an infinite loop if we use it inside the `effect()` function.

## Signals have no "complete" state

Because of that fact, this code:

```ts
const $v = computed(() => $foo() ?? $bar() ?? 0);
```

is not equal to:

```ts
const v$ = merge(foo$, bar$).pipe(
  map((val) => val ?? 0)
);
```

or to:

```ts
const v$ = race(foo$, bar$).pipe(
  map((val) => val ?? 0)
);
```

Because when one of the passed observables is complete, `merge()` will keep emitting values of non-complete observables and will ignore the last values of completed observables. 

`race()` will unsubscribe from all observables except the one that emitted its value first, and will keep emitting only values of that observable.

Every RxJS operator cares if an input observable is complete or has an error - Signals don't care about such nuances. And it's not a good or bad thing - it's just a difference, sometimes we want to care about completeness and errors (as with XHR requests), and sometimes we don't.

## Signals are always synchronous

The fact that Signals are synchronous and should always be able to return a value, doesn't let us implement some operators using Signals:

* `forkJoin()`
* `concat()`
* `debounce[Time]()`
* `switchMap()` (and siblings)
* `distinctUntilChanged()`
* `filter()`
* `skip()`
* `takeUntil()`

and many others. Everyone who used them knows how powerful these operators are.

That's why I picked that painting (one of my favorites) for this article: Observables and Signals are better to be used together, you can use both of their powers and avoid their trade-offs.

---
💙 _If you enjoy my articles, consider [following me on Twitter](https://twitter.com/eugeniyoz), and/or [subscribing](https://medium.com/@eugeniyoz) to receive my new articles by email._  
🎩️ _If you or your company is looking for an Angular consultant, you can [purchase my consultations on Upwork](https://www.upwork.com/services/consultation/development-it-evgeniy-1696285343741362176?ref=project_share)._