Observable and Signal
If you've not started the Observable part yet, we recommend you to read it first, and then come back here.
Signals are not a replacement but a complement to Observable.
That's why, we may cast a Signal to an Observable and vice-versa.
fromSignal(...)
function fromSignal<GValue>(
signal: IReadonlySignal<GValue>,
options?: IFromSignalOptionsForValueMode<GValue>,
): IObservable<GValue>;
function fromSignal<GValue>(
signal: IReadonlySignal<GValue>,
options: IFromSignalOptionsForNotificationMode<GValue>,
): IObservable<IObservableFromSignalNotifications<GValue>>;
With:
interface IFromSignalOptionsForValueMode<GValue> {
readonly mode?: 'value';
readonly onError?: IObservableFromSignalOnErrorFunction<GValue>;
}
interface IFromSignalOptionsForNotificationMode<GValue> {
readonly mode: 'notification';
}
The fromSignal
function is used to convert a Signal to an Observable.
It has two modes:
value
: returns an Observables sending only the values of the Signal. AnonError
function may be given (IObservableFromSignalOnErrorFunction
), which is called when the signal enters in an "error" state, and allows us to return a different value or discard it (by default, it logs the error and discards it).notification
: returns an Observables sendingnext
notifications when the signal emits a value, anderror
notifications when it throws errors.
Example: cast a Signal to an Observable
const counter = signal(0);
const counter$ = fromSignal(counter);
counter$((count: number) => {
console.log(`count: ${count}`);
});
window.onclick = () => {
counter.set(counter() + 1);
};
Outputs:
count: 0
// on click
count: 1
// ...
toSignal(...)
Conversely, it is possible to convert an Observable to a Signal by using the toSignal
function.
function toSignal<GValue>(
value$: IObservable<GValue>,
options?: ICreateSignalFromValueObservableOptions<GValue>,
): ISignalFromObservable<GValue>;
function toSignal<GValue>(
value$: IObservable<IDefaultInNotificationsUnion<GValue>>,
options: ICreateSignalFromNotificationsObservableOptions<GValue>,
): ISignalFromObservable<GValue>;
interface ISignalFromObservable<GValue> extends IReadonlySignal<GValue> {
isActive(): boolean;
activate(active?: boolean): void;
}
interface ICreateSignalFromObservableSharedOptions<GValue>
extends ICreateSignalOptions<GValue> {
readonly initialValue?: GValue;
}
interface ICreateSignalFromValueObservableOptions<GValue>
extends ICreateSignalFromObservableSharedOptions<GValue> {
readonly mode?: 'value'; // (default: 'value')
}
interface ICreateSignalFromNotificationsObservableOptions<GValue>
extends ICreateSignalFromObservableSharedOptions<GValue> {
readonly mode: 'notification';
readonly unsubscribeOnError?: boolean; // (default: true)
}
The toSignal
function internally subscribes to the given Observable and updates the returned Signal any time the Observable emits a value or notification.
Options
The toSignal
function accepts 2 modes:
value
(default): when a value is sent by the Observable, the Signal is updated with this value.notification
: when a notification is sent by the Observable- if it's a
next
notification, then the Signal is updated with this notification's value - else in case of
error
notification, the signal enters an "error" state. IfunsubscribeOnError
is true, then the provided Observable will unsubscribe too.
- if it's a
You may provide an initialValue
too.
Returned Signal
The returned Signal is readonly and has two more methods:
isActive()
This method returns true
if this Signal is currently subscribed to the Observable.
activate(...)
- if
true
, or not argument, is provided to this method, then the Signal subscribes to the Observable. When a value is sent by the Observable, the Signal is updated with this value. - if
false
is given, then the Signal unsubscribes of the Observable.
activate(true)
is called internally in the toSignal
function, so you don't have to call it immediately after creating the Signal from the Observable.
By nature, a Signal has no cancellation as opposed to an Observable. This is why an activate
function is required.
You must call activate(false)
when you want to dispose of the Signal, to free resources and avoir memory leaks.
Examples
Synchronous vs Asynchronous Observables
Some Observables are known to emit synchronously, which is the desired behaviour for a Signal.
In those cases, you may get the signal's value immediately:
// this emits synchronously:
const count$ = single(50);
const count = toSignal(count$);
console.log(count());
However, if count$
is ever made asynchronous (such as by adding a debounce operation, for example),
getting the signal's value before the Observable emits, will throw an error:
// this emits asynchronously:
const count$ = debounceTime$$(single(50), 100);
const count = toSignal(count$);
console.log(count()); // throws !
Initial Values
If an Observable is known to emit only asynchronously, then, we may provide an initialValue
to the signal:
// the first value will not be emitted until 1 second later
const seconds$ = scan$$(interval(1000), _ => _ + 1, 0);
// provide an initial value of zero
const seconds = toSignal(seconds$, { initialValue: 0 });
effect(() => {
console.log(seconds());
});
Observable of Notifications
const requestSignal = toSignal(fromFetch('https://example.com'), { mode: 'notification', initialValue: null });
effect(() => {
try {
const response = requestSignal();
if (response !== null) {
// Handle the response here
}
} catch (e: unknown) {
// Handle the error from the observable here
}
});