We use cookies to enhance your experience on the site
CodeWorlds

Tower and messenger - signals next to streams

All through this module you have been raising signal towers. A signal answers one question and answers it at once: how much is there right now. You call

level()
and you get the number in the same millisecond, without waiting and without asking anyone for permission. That is what state looks like.

There is news, though, that will not fit inside that question. The bell at the Academy's main gate strikes again and again all day long - that is not one value but a sequence of strikes spread out over time. A messenger sent to the neighbouring castle will be back in three days, or will never come back at all. A scout on the road reports five times over one afternoon and then falls silent until evening. If you wanted to describe things like that with a tower, you would have to ask "how many bell strikes right now" and get an answer that says nothing about the strikes still to come.

You have already seen one such sequence, @name, on the Tokaido roads. When you read route parameters through

route.paramMap
without a snapshot, you did not get a value; you got an object you had to sign up to with the
subscribe()
method, so that Angular would call your function on every fresh piece of news. That object is called an
Observable
, a stream, and the whole library of tools for working with such streams - RxJS - is waiting for you in the next module, among the ninja scouts. Today you are not learning RxJS. Today you are learning the thing without which streams and signals would live in two separate castles: how to convert one into the other, and why you would reach for either.

One small convention before we set off, because you will meet it in every example. In Angular, names of variables holding a stream end with a dollar sign:

timer$
,
query$
,
events$
. It is a convention for human eyes only, the compiler does not pay it the slightest attention, but thanks to it you can tell at first glance that there is a sequence of values inside rather than a single number.

A bridge built by hand

Let us start with the trouble, because the trouble is what explains the rest of the lesson. You want to show the number of bell strikes in a template, and the strikes arrive as a stream. An Angular template cannot read a stream the way it reads a signal - there are no parentheses there that would return anything. So you have to build a bridge: subscribe to the stream and write every incoming value into a signal that the template already understands.

To build the stream I will use the

interval
function from the RxJS library. You give it a number of milliseconds and it hands back a stream that emits the next number every that many milliseconds: zero, one, two, three. That is all you need to know about it today. The rest of the code below you already know: the
@Component
decorator with its
selector
,
standalone
and
template
fields, a signal from
signal()
, and the
set()
method from the earlier stops in this module.

1import { Component, signal } from '@angular/core';
2import { interval } from 'rxjs';
3
4@Component({
5  selector: 'app-gate-bell',
6  standalone: true,
7  template: '<p>Bell strikes: {{ ticks() }}</p>'
8})
9export class GateBellComponent {
10  ticks = signal(0);
11
12  constructor() {
13    interval(1000).subscribe(value => {
14      this.ticks.set(value);
15    });
16  }
17}

This code works: every second the stream emits a new number, the subscription receives it,

set()
writes it into the signal and the template shows the next strike. Notice what this construction did not change. The template reads
ticks()
in exactly the same way as every other signal in this module, and has no idea at all that the value came in from outside. The whole price sits somewhere else: you wrote three things by hand instead of one. A field for the value, a subscription, and - this is the easiest one to forget - a debt to be settled, because a subscription ought to be closed when the component is destroyed, otherwise the bell goes on ringing for a component that is no longer on the screen. The same bridge rewritten in twenty components is twenty chances to forget about something once.

toSignal - the signaller at the gate

In the Academy nobody makes the commander run out for dispatches himself. A signaller stands at the gate: he takes every scroll a messenger brings and turns it into smoke above the tower straight away. The commander watches nothing but the tower and never has to know how many messengers are out on the road. Angular has exactly that signaller and its name is

toSignal()
.

The

toSignal()
function takes a stream and returns a signal that always shows the latest value from that stream. There is one direction and only one: from the stream to the signal, from the messenger onto the tower. It lives in a separate package,
@angular/core/rxjs-interop
, and not in the
@angular/core
you know - Angular keeps its bridges to RxJS off to one side, so that an application which does not use RxJS never has to pull it in. The whole import line is made of four pieces in an order that never varies: the word
import
, the brace with the name
{ toSignal }
, the word
from
, and at the end the package name in quotes,
'@angular/core/rxjs-interop'
.

The second, optional argument is an options object. Today you need one entry out of it:

initialValue
, the value the signal is to show before the stream has said anything at all.

1import { Component } from '@angular/core';
2import { toSignal } from '@angular/core/rxjs-interop';
3import { interval } from 'rxjs';
4
5@Component({
6  selector: 'app-gate-bell',
7  standalone: true,
8  template: '<p>Bell strikes: {{ ticks() }}</p>'
9})
10export class GateBellComponent {
11  private timer$ = interval(1000);
12
13  ticks = toSignal(this.timer$, { initialValue: 0 });
14}

Three lines turned into one, and that is the whole of the change you can see with the naked eye. What did not change is far more interesting. The template looks the same character for character, because

ticks
is still an ordinary signal read through parentheses. The stream stayed a stream as well:
timer$
knows nothing about signals and you can still hand it to anybody else. What disappeared is both of the things you had to keep an eye on: the subscription is now opened by
toSignal()
, and closed by it too, when the component is destroyed. That is precisely why this call has to stand where Angular knows whose destruction is meant - that is, in a class field or in the constructor, in the same injection context in which you called
inject()
back in the module about guilds.

One more limitation is worth remembering right away, because it does catch people out. The signal returned by

toSignal()
is read-only: it has neither
set()
nor
update()
. I checked this with the compiler - an attempt to call
ticks.set(5)
ends with the error "Property 'set' does not exist on type 'Signal<number>'" and the application does not build. This is not a whim but a consequence: the value is dictated by the stream, so if you were allowed to write something into that signal from the side, the messenger's next report would overwrite it anyway. The call itself always has the same shape of four pieces: the name
toSignal
, an opening parenthesis, the stream name
timer$
, a closing parenthesis.

When the stream has not said anything yet

The

initialValue
option is not compulsory, so the natural question is: what does the signal show if you leave it out and the messenger has only just set off? Not zero, and not some empty value invented by Angular. In that situation
toSignal()
returns a signal whose type is "the value or
undefined
", and the very first read gives you exactly
undefined
.

1private timer$ = interval(1000);
2
3ticks = toSignal(this.timer$);

I ran both variants side by side and the outcome is unambiguous: the version without options returns

undefined
immediately after it is created, while the version with
{ initialValue: 0 }
returns zero. After two strikes both already show the same thing, because from the first report onwards there is no difference between them whatsoever. The type did change, though, and that shows up across the whole component: without a starting value TypeScript will question you about
undefined
in every single place where you reach for a field of the result.

You have three ways out and I do have a favourite among them. The first is

initialValue
. The second is leaving
undefined
in place and covering the view with an
@if
condition in the template. The third,
{ requireSync: true }
, is for streams that emit a value the instant you subscribe - the type is clean then, but at the price of a risk: I tried it on the
interval
stream, which does not answer instantly, and Angular threw error NG0601 with a message saying the Observable did not emit synchronously. My advice, @name: for continuous streams such as the bell or mouse events, pass
initialValue
, because zero strikes is a truth about the world. For a one-off answer from the network leave
undefined
, because it honestly means "the data is not here yet", whereas an artificial empty array would lie that the data has already arrived and is empty.

A report from the other end of the world

The commonest stream in a real application does not tick every second - it brings one answer from the server and falls silent. Talking to a server in Angular is the job of the

HttpClient
service, which has a whole later module devoted to it, the one about courier messengers. Today one trait of it is enough for you: the
get()
method returns nothing straight away. It hands back a stream that will emit one value once the response arrives, and end there. You obtain the service through
inject()
from the module about guilds, and in the angle brackets after
get
you state the type of data you are expecting.

Let us first agree on what that data is. The

User
interface describes a single user: it has an
id
field of type
number
, so that one can be told apart from another, and a
name
field of type
string
. The server returns a whole list of them, so the response type is
User[]
. In the template I use the
@if
and
@for
blocks you know from the module about components.

1import { Component, inject } from '@angular/core';
2import { HttpClient } from '@angular/common/http';
3import { toSignal } from '@angular/core/rxjs-interop';
4
5interface User {
6  id: number;
7  name: string;
8}
9
10@Component({
11  selector: 'app-users',
12  standalone: true,
13  template: `
14    @if (users()) {
15      @for (user of users(); track user.id) {
16        <p>{{ user.name }}</p>
17      }
18    }
19  `
20})
21export class UsersComponent {
22  private http = inject(HttpClient);
23
24  users = toSignal(this.http.get<User[]>('/api/users'));
25}

The whole component is now two fields and not a single method. The

@if (users())
condition is not decoration - it stands there precisely because I did not pass
initialValue
, so until the answer arrives
users()
returns
undefined
and the
@for
loop would have nothing to walk over. The compulsory
track
tells Angular what to recognise the same row by between refreshes, and here that is
user.id
. Now pay attention to what
toSignal()
did not do, because this is where most of the illusions are born. It did not send the request -
HttpClient
did. It did not retry it after an error, it did not wait a moment first, it did not remember the result for later and it cannot ask a second time. The signaller at the gate only turns dispatches into smoke, and that particular dispatch is one nobody will bring again.

The four steps of the bridge

Let us stop for a moment and lay the whole road out in order, because this is one of those sequences worth being able to recite. First a stream comes into being, for example

interval(1000)
. Then
toSignal()
turns it into a signal. Then you read the value by calling that signal with parentheses -
timerSignal()
if you name the field a timer, or
ticks()
in our bell. And at the end
effect()
from the earlier lesson in this module reacts to every change of it, because it tracks every signal read inside its own body.

Let us add that last step to the bell component. The whole effect is a single call in the constructor, in exactly the same place where you set your effects up in the previous lessons.

1constructor() {
2  effect(() => {
3    console.log('Bell strike number', this.ticks());
4  });
5}

From now on every bell strike reaches the console. The most important thing is what is not in that effect: not a word about a stream, no

subscribe()
, no
interval
, not even an import from RxJS. The effect sees an ordinary signal and has no notion that a messenger is running on the other side of the bridge. That is exactly the point of this whole bridge: once a stream has become a signal, everything you have learned in this module - reading with parentheses, derived values from
computed()
, effects - works on it without a single change.

toObservable - a messenger sent out from the tower

The bridge is sometimes needed the other way round too, and that is not art for art's sake. Picture the search box for warriors in the Academy archive. A student types letter after letter, and you want to wait three hundred milliseconds from the last character before setting off for the server, and to try once more if the connection drops. A signal will not do that. A signal can answer what is there right now - it cannot wait, measure out time or retry, because it does not know the notion of "later". RxJS operators do know it, and you will hear about them among the ninja scouts. To be able to use them, though, you first have to turn the tower back into a messenger.

That is the job of

toObservable()
, the second function from the same
@angular/core/rxjs-interop
package. It takes a signal and returns a stream that emits the successive values of that signal. In the template below, the
(input)
binding fires on every keystroke and writes the contents of the field into the signal;
$any($event.target)
is only there to tell TypeScript to stop asking what kind of element the event came from.

1import { Component, signal } from '@angular/core';
2import { toObservable } from '@angular/core/rxjs-interop';
3
4@Component({
5  selector: 'app-search-box',
6  standalone: true,
7  template: '<input (input)="query.set($any($event.target).value)">'
8})
9export class SearchBoxComponent {
10  query = signal('');
11
12  query$ = toObservable(this.query);
13}

You now have two handles on the same thing:

query
for the template and
query$
for the world of streams. Nothing changed in the signal itself along the way -
query
is still writable, you still read it with parentheses, and it is still the source of truth. Memorise this pair of names, because mixing them up is the commonest slip in this whole topic:
toSignal()
leads from a stream to a signal,
toObservable()
from a signal to a stream. The compiler is merciless here and that is good news. I checked both wrong usages: passing a signal to
toSignal()
ends with a message that
WritableSignal
does not fit a parameter of type
Observable
, and passing a stream to
toObservable()
- that
Observable
does not fit a parameter of type
Signal
. Neither of these functions, on the other hand, is there to synchronise signals between components, because a shared service from the module about guilds is what does that, nor to debug anything.

There is one thing I will not invent here, because it cannot be run outside a full application: I did not run this stream in the console. What does follow from the type documentation is that

toObservable()
passes values on through an effect, and therefore needs an injection context, exactly like
toSignal()
. The practical conclusion is that the stream reports a settled value rather than every single write - if you set the signal three times in one instant, the far side will see the final state.

Tower or messenger - how to choose

Since the two worlds can be joined with a single line, the most important question remains: why reach for a stream at all, when signals are simpler? Master Ango-san always gives the same counsel on this, and I recommend that order to you as a ready-made decision process, @name.

Question one: is this a synchronous and simple value, the kind you can ask "how much right now" about? A warrior's level, the selected row, the flag for an open panel. Then you take

signal()
and that is the end of it. Question two: is that value derived from other signals, that is, can it be worked out from them? Then you take
computed()
and never think about it again. Question three: is it an asynchronous stream in which time and order matter - answers from a server, messages from a WebSocket, a sequence of keyboard events? Here the territory of RxJS begins. Question four, the closing one: do you already have a stream and need it in the template as a signal? Then you set up
toSignal()
and return to the world of towers.

Let us test that on four concrete tasks, because only then does the rule settle in your head. Storing simple loading and error state is a job for a signal - those are two values you ask "how are things right now" about, and no time occurs in them. A synchronous counter value inside a component is a signal as well, a textbook one: the number exists at this moment and will not change of its own accord. A simple derived calculation from two values is

computed()
, because Angular will recompute it itself after every change of the ingredients. But a stream of events with a wait after the last character and a retry after an error - our search box, that is - is a task no signal will carry out. Here you need tools that operate on time and on the order of events, and only RxJS has those. You will learn the names of those tools in the next module; today remember the criterion itself: time and order are the territory of streams, state here and now is the territory of signals.

linkedSignal - a board you are allowed to correct in chalk

Angular 19 added two new tools to the arsenal, and both of them solve troubles that have already turned up in this lesson. The first one looks like this. On the board in the council hall hangs the name of the squad chosen to march out. By default it is the first squad on the roll, but the commander has every right to cross it out and write a different one in chalk. When a new roll of squads arrives from the castle, however, the board has to be rewritten from scratch - the old choice may no longer exist.

None of the tools you know does that. A derived value from

computed()
recomputes itself, but it is read-only and the commander will correct nothing on it - I checked this with the compiler, and calling
set()
on such a value ends with an error saying that no such property exists. An ordinary signal from
signal()
is the opposite: it lets you write, but it will never learn about the new roll and will go on holding the name of a disbanded squad. You need something in between, and that is exactly
linkedSignal()
- a writable signal which at the same time resets itself when its source changes. You give it a function computing the starting value, exactly as with
computed()
.

1import { signal, linkedSignal } from '@angular/core';
2
3const squads = signal(['Hikari', 'Kaze', 'Yama']);
4
5const selected = linkedSignal(() => squads()[0]);

I ran this code: the first read of

selected()
returns the string "Hikari", the first squad on the roll. So far it looks like an ordinary derived value, and there is nothing strange in that, because at this moment it behaves identically. The difference only shows itself when you write, and when the roll is swapped.

1selected.set('Yama');
2
3squads.set(['Tora', 'Ryu']);

I checked both calls on the running code. After the first one

selected()
returns "Yama" - so the write succeeded, which with
computed()
would not even compile. After the second one
selected()
returns "Tora", the first squad from the new roll. Nobody set that: the change of the source invalidated the chalk note by itself, and the board was rewritten from the beginning. And once again the most important part is what did not change - the
squads
signal knows nothing of
selected
's existence and has no agreement with it, and after the reset
selected
stays fully writable, so the commander can write "Ryu" a moment later.

Three other descriptions of

linkedSignal()
sound plausible, and all three are false. It is not a constant signal whose value cannot be changed - you have just set it twice, and the
set
and
update
methods are both present on it, which I checked on the running object. Nor does it connect two components through a shared service; sharing state between components is the job of the service from the module about guilds, while
linkedSignal()
lives quietly in one file and knows nothing of any components. And it does not convert streams into signals - it comes from the
@angular/core
package, not from the bridge to RxJS, and has no notion that
Observable
exists. Turning a stream into a signal is the job of
toSignal()
, and of nothing else.

resource() - an expedition for a scroll

The second tool from Angular 19 goes back to a matter we left unfinished with the list of users. That bridge worked as long as the data was fetched once. But what if the component shows the card of a single warrior, the warrior's number arrives from outside through

input.required()
from the previous lesson, and that number changes? The stream from
http.get()
brought its one answer and fell silent for good. You would have to build a new stream for the new number and abandon the previous one halfway - it can be done with RxJS operators, which you will hear about in the next module, but you have to know how.

Angular 19 gives an answer designed for signals from the start: the

resource()
function. Think of it as an expedition to a distant library to fetch a scroll. You give it an object with two fields. The
request
field is a function saying what for we are setting off - you read signals inside it, and Angular sees to it that a change in any of them sends the expedition out anew. The
loader
field is a function saying how to obtain the data; it is asynchronous, so it is marked
async
and waits for the answer with
await
, and it receives the current
request
as its argument. Inside it I use the browser's
fetch
function and turn the response into an object with the
json()
method, declaring the result as a
Promise
of a
Samurai
. A warrior's card is the
Samurai
interface in our code, with the fields
id
and
name
. Below you see the class field on its own - the rest of the component, together with
id = input.required<number>()
, looks just as it did in the previous lesson.

1samurai = resource({
2  request: () => this.id(),
3  loader: async ({ request: id }) => {
4    const response = await fetch(`/api/samurai/${id}`);
5    return response.json() as Promise<Samurai>;
6  }
7});

The

{ request: id }
notation in the function header pulls the
request
field out of the argument and gives it the shorter name
id
. I compiled all of this code and it passes without complaint. I did not run it, however, because
resource()
needs a working Angular application - trying to call it in bare Node ends with an error about a missing internal provider. So rather than invent results, I will say outright what happens: when
this.id()
changes value, Angular calls
loader
once more for the new number, and aborts the previous, unfinished expedition. Nothing changed along the way in how the data is fetched - inside there is still the ordinary
fetch
you wrote yourself.

You read the outcome of the expedition with signals, and that is the whole reward for this notation. The

value()
field returns the data once it is there. The
error()
field returns the error if the expedition failed. The
isLoading()
field returns true or false and is most convenient in a template. And the
status()
field reports the phase of the whole expedition - we will say more about that one in a moment. The template below chains the three cases with
@if
,
@else if
and
@else
.

1@if (samurai.isLoading()) {
2  <p>Messenger on the road...</p>
3} @else if (samurai.error()) {
4  <p>No report</p>
5} @else {
6  <p>{{ samurai.value()?.name }}</p>
7}

This template handles all three endings of the expedition and needs not a single helper field in the class. The question mark in

value()?.name
is necessary, because until the data has arrived
value()
returns
undefined
- the same caution that
toSignal()
without a starting value forced on you earlier.

That leaves the promised phase,

status()
. It reports what stage the expedition has reached: whether the data is still loading, whether it has arrived and we have success, or whether it ended in an error. It is just as worth knowing what is not there: there is no suspension phase, no "paused". An expedition for a scroll cannot be halted halfway and told to wait for a signal - at most it can be repeated with the
reload()
method, or aborted along with the component. In fairness I will add that in Angular 19 the whole of
resource()
is marked experimental, and the exact spelling of these phase names was still being refined between releases. So remember the meaning rather than the letters: loading, success, error - and not a trace of a pause. The names in force in your version you can always check in the Angular documentation.

Remember one thing from this lesson, @name: a tower answers how things stand right now, a messenger brings news spread out over time, and

toSignal()
and
toObservable()
are the only two bridges between them - one from the road onto the walls, the other from the walls onto the road.

Go to CodeWorlds