Skip to content

derivedFrom

Created by Enea Jahollari

derivedFrom is a helper function that combines the values of Observables or Signals and emits the combined value. It also gives us the possibility to change the combined value before emitting it using rxjs operators.

It is similar to combineLatest, but it also takes Signals into consideration.

import { derivedFrom } from 'ngxtension/derived-from';

Usage

derivedFrom accepts an array or object of Observables or Signals and returns a Signal that emits the combined value of the Observables or Signals. By default, it needs to be called in an injection context, but it can also be called outside of it by passing the Injector in the third argument options object. If your Observable doesn’t emit synchronously, you can use the startWith operator to change the starting value, or pass an initialValue in the third argument options object.

const a = signal(1);
const b$ = new BehaviorSubject(2);
// array type
const combined = derivedFrom([a, b$]);
console.log(combined()); // [1, 2]
// object type
const combined = derivedFrom({ a, b: b$ });
console.log(combined()); // { a: 1, b: 2 }

It can be used in multiple ways:

  1. Combine multiple Signals
  2. Combine multiple Observables
  3. Combine multiple Signals and Observables
  4. Using initialValue param
  5. Use it outside of an injection context

1. Combine multiple Signals

We can use derivedFrom to combine multiple Signals into one Signal, which will emit the combined value of the Signals.

const page = signal(1);
const filters = signal({ name: 'John' });
const combined = derivedFrom([page, filters]);
console.log(combined()); // [1, { name: 'John' }]

At this point we still don’t get any benefit from using derivedFrom because we can already combine multiple Signals using computed function. But, what’s better is that derivedFrom allows us to change the combined value before emitting it using rxjs operators (applying asynchronous operations), which is not possible with computed.

import { derivedFrom } from 'ngxtension/derived-from';
import { delay, of, pipe, switchMap } from 'rxjs';
let a = signal(1);
let b = signal(2);
let c = derivedFrom(
[a, b],
pipe(
switchMap(
([a, b]) =>
// of(a + b) is supposed to be an asynchronous operation (e.g. http request)
of(a + b).pipe(delay(1000)), // delay the emission of the combined value by 1 second for demonstration purposes
),
),
);
effect(() => console.log(c())); // 👈 will throw an error!! 💥
setTimeout(() => {
a.set(3);
}, 3000);
// You can copy the above example inside an Angular constructor and see the result in the console.

This will throw an error because the operation pipeline will produce an observable that will not have a sync value because they emit their values later on, so the resulting c signal doesn’t have an initial value, and this causes the error.

You can solve this by using the initialValue param in the third argument options object, to define the starting value of the resulting Signal and prevent throwing an error in case of real async observable.

let c = derivedFrom(
[a, b],
pipe(
switchMap(
([a, b]) => of(a + b).pipe(delay(1000)), // later async emit value
),
),
{ initialValue: 42 }, // 👈 pass the initial value of the resulting signal
);

This works, and you can copy the above example inside a component constructor and see the result in the console:

42 - // initial value passed as third argument
3 - // combined value after 1 second
5; // combined value after 3 seconds

Another way to solve this problem is using the startWith rxjs operator in the pipe to force the observable to have a starting value like below.

let c = derivedFrom(
[a, b],
pipe(
switchMap(([a, b]) => of(a + b).pipe(delay(1000))),
startWith(0), // 👈 change the starting value (emits synchronously)
),
);

The console log will be:

0 - // starting value (initial sync emit)
3 - // combined value after 1 second
5; // combined value after 3 seconds

2. Combine multiple Observables

We can use derivedFrom to combine multiple Observables into one Signal, which will emit the combined value of the Observables.

const page$ = new BehaviorSubject(1);
const filters$ = new BehaviorSubject({ name: 'John' });
const combined = derivedFrom([page$, filters$]);
console.log(combined()); // [1, { name: 'John' }]

This is just a better version of:

const combined = toSignal(combineLatest([page$, filters$]));

And it can be used in the same way as in the previous example with rxjs operators.

3. Combine multiple Signals and Observables

This is where derivedFrom shines. We can use it to combine multiple Signals and Observables into one Signal.

const page = signal(1);
const filters$ = new BehaviorSubject({ name: 'John' });
const combined = derivedFrom([page, filters$]);
console.log(combined()); // [1, { name: 'John' }]
// or using the object notation
const combinedObject = derivedFrom({ page, filters: filters$ });
console.log(combinedObject()); // { page: 1, filters: { name: 'John' } }
const page$ = new Subject<number>(); // Subject doesn't have an initial value
const filters$ = new BehaviorSubject({ name: 'John' });
const combined = derivedFrom([page$, filters$]); // 👈 will throw an error!! 💥

But, we can always use the startWith operator to change the initial value.

const combined = derivedFrom([
page$.pipe(startWith(0)), // change the initial value to 0
filters$,
]);
console.log(combined()); // [0, { name: 'John' }]

4. Using initialValue param

Or you can pass initialValue to derivedFrom in the third argument options object, to define the starting value of the resulting Signal and prevent throwing error in case of observables that emit later.

const combined = derivedFrom(
[page$, filters$],
switchMap(([page, filters]) => this.dataService.getArrInfo$(page, filters)),
{ initialValue: [] as Info[] }, // define the initial value of resulting signal
); // inferred ad Signal<Info[]>

5. Use it outside of an injection context

By default, derivedFrom needs to be called in an injection context, but it can also be called outside of it by passing the Injector in the third argument options object.

@Component()
export class MyComponent {
private readonly injector = inject(Injector);
private readonly dataService = inject(DataService);
// we can read the userId inside ngOnInit and not in the constructor
@Input() userId!: number;
data!: Signal<string[]>;
ngOnInit() {
// not an injection context
const page = signal(1);
const filters$ = new BehaviorSubject({ name: 'John' });
this.data = derivedFrom(
[page, filters$],
pipe(
switchMap(([page, filters]) =>
this.dataService.getUserData(this.userId, page, filters),
),
startWith([] as string[]), // change the initial value
),
{ injector: this.injector }, // 👈 pass the injector in the options object
);
}
}