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.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.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.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.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.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.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.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.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.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.