We use cookies to enhance your experience on the site
CodeWorlds

Smoke signals - the first tower on the walls

The training panel from the second module worked without a hitch, @name. You clicked a button, a method added the repetitions to a class field, and the number on the screen went up. An ordinary field, an ordinary assignment, no tricks anywhere - and that is exactly why one uncomfortable question is worth asking: who actually told Angular that this number had changed?

Nobody did. A number in a class field is a number and nothing more. It has no mouth, no messenger of its own, and no way of letting anyone know that a moment ago it was a ten and now it is a twelve. Angular copes with that the only way left to it: after every click, every timer tick and every answer coming back from the network it walks the entire component tree and checks, one by one, every expression written into the templates, comparing each result with the previous one. If nobody reports anything, you have to go round everybody and ask.

In the Academy this arrangement has a name and a price. For years the news between strongholds was carried by a messenger: he set off after the smallest disturbance, walked round every castle in turn and asked at each gate whether anything had changed. With three castles nobody complained. With a hundred the messenger became the slowest link in the whole system - not because he ran badly, but because he ran everywhere, including the places where nothing had happened for a week.

That is why signal towers were raised along the walls. A tower does not wait for a messenger. When the guard changes what the tower shows, the smoke goes up at once, and the ones who find out are exactly those who were watching that tower - and nobody else. In Angular such a tower is called a signal: a value that knows by itself that it has changed, and can by itself notify everyone who has read it. Signals arrived in Angular 16, settled down in 17, and in 19, the version you are working on, they are already the basic way of holding state. I mentioned them back in the first module, in the survey of what sets Angular apart today - today you light your first tower with your own hands.

What you will learn

  • raising a signal with the
    signal()
    function and giving it a starting value
  • reading a signal's value and understanding why that read is written with parentheses
  • telling a signal apart from an ordinary class field, including by what the compiler says
  • issuing the
    set()
    order, that is, setting the value outright
  • reading signals in a component template exactly as you do in class code
  • recognising notations that look like signals but are nothing of the sort

Lighting the tower

The

signal()
function is handed to you by the
@angular/core
package - the very same one you have been taking the
@Component
decorator from since the first module. You give it one argument: the value the tower is to show at the start. Let us agree on an example that will carry us through half of this lesson. The guard at the gate keeps an alert level: a number that sits at zero while the road is quiet and climbs when the scouts bring back disturbing reports. Since we begin in peace, the starting value will be zero.

1import { signal } from '@angular/core';
2
3const alertLevel = signal(0);

The tower stands, and it shows zero. Notice what this line does not contain. There is no

new
keyword, because
signal
is not a class but an ordinary function that builds a signal and hands it back as its result. There is no decorator above a field, because a signal is not an annotation glued onto a variable. And finally there is no type declaration at all, yet TypeScript already knows that a number lives in this tower - it worked that out from the zero you supplied. From this moment on, an attempt to write a string in there ends with a compilation error before the application even starts. That is the same discipline you met in the first module with the Bushido code of programming, only tucked away behind a single call.

Commit the call itself to memory as four pieces always laid out in the same order: the name

signal
, an opening parenthesis, the starting value, a closing parenthesis. Nothing before the name, nothing between the parenthesis and the value. A signal counting from zero is therefore literally
signal
, then
(
, then
0
, then
)
.

Three other notations look every bit as sensible and not one of them works - I checked each with the compiler. The form

new Signal(0)
has no chance of compiling, because
Signal
in the
@angular/core
package is a type name, not a class; the compiler answers outright that Signal only refers to a type here and is being used as a value. The form
createSignal(0)
is a name carried over from an entirely different framework, since
@angular/core
exports no such function - the compiler says the module has no such export, and inspecting the package itself while the program runs confirms it: the name
createSignal
is simply not in there. The form
@Signal() count = 0
transplants onto signals a habit picked up from the decorators you know from
@Component
and
@Injectable
- except that Angular has no decorator by that name, so the compiler raises an error whether or not you import the name
Signal
at all. One road remains, and it is the right answer: a plain call to the
signal()
function.

Why the read has parentheses

The tower stands, but nobody has looked at it yet. Reading a signal is written as the name followed by an empty pair of parentheses, which surprises everyone used to class fields: it looks like a method call, even though you are neither computing nor changing anything here.

1console.log(alertLevel());  // 0

It printed zero, and that is a result from code I actually ran, not from memory. The reason for the parentheses is beautifully simple and worth holding on to for the rest of the module: a signal really is a function. Not an object with a field inside it, not a variable pretending to be a number - a function you call in order to get the current value. That call does one more thing, though, invisible to the naked eye. If it happens in a place Angular is tracking, and a component template is exactly such a place, the read registers a dependency: Angular notes down that this particular fragment of the view is watching this particular tower. Thanks to that, at the next change it does not have to walk round every castle, because it knows whom to wake. I checked this on running code: a value built on one signal recomputed only after that signal changed, and a write to a signal it had never read did not stir it even once. Outside such a tracked place the read is the plainest possible fetch of a value - it is synchronous, nothing is requested from a server, and three calls in a row return the same number and consume nothing.

Since a signal is a function, that can be verified in the simplest way imaginable. The

typeof
operator answers in a single word what kind of value it is handed: for a number it says "number", for a function it says "function". Let us put the question to it twice - once about the tower itself, once about what we read off it.

1console.log(typeof alertLevel);    // function
2console.log(typeof alertLevel());  // number

The first line says "function", the second "number", and that difference holds the entire answer to the question about parentheses. Without the parentheses you are holding the tower; with them, what can be seen on it. Both results were printed by code that was run. Notice what this check did not change: the signal stayed exactly as it was, its value is still zero, and peering into a tower disturbs nothing inside it.

A story does the rounds alongside this, that the parentheses have to be written because otherwise TypeScript will refuse to compile. That is untrue, and the real reason is more interesting. The line

console.log(alertLevel)
without parentheses compiles without a single warning - I checked that with the compiler - it is just that you are then passing on the function itself instead of the number. The error only appears when you try to use the tower where a number is expected: the assignment
const x: number = alertLevel
is rejected by the compiler, which explains that a signal of a number cannot be substituted for a number. So the parentheses are not forced on you by the compiler - they are a consequence of what a signal is.

That leaves the last myth, and the most innocent: that a signal is simply a variable held somewhere in memory, and the parentheses are decoration. If that were so, the value could be pulled out by one of the customary routes - a

value
field, a
get()
method or a
read()
method. So let us check whether any of those three names exists at all.

1console.log(typeof alertLevel.value);  // undefined
2console.log(typeof alertLevel.get);    // undefined
3console.log(typeof alertLevel.read);   // undefined

Three times "undefined", and that is a result from running code: none of those names is on the signal. The compiler is even blunter, because for each of them it replies that no such property exists on a signal of a number. The name

value
did not come out of thin air - that is how a value is read in Vue's reactivity, while
get()
and
read()
are habits carried over from other libraries. In Angular there is exactly one read, and it is written as two empty parentheses after the signal's name. Nothing in the tower itself changed along the way: those three enquiries set nothing and erased nothing.

Order one: set the value

A tower that shows zero for its whole tour of duty is no more than an ornament on the wall. A scout comes back from the pass and reports a foreign detachment sighted on the road, so the alert level has to go up. Instinct suggests the notation known from class fields, an assignment with an equals sign - and with a signal that instinct is wrong in two ways at once. The line

alertLevel = 2
does not change what the tower shows: it swaps the whole tower for the number two, and so destroys the signal instead of updating it. In practice it never even gets that far, because the compiler rejects such a line, explaining that a number cannot be substituted for a signal of a number - I checked it on a class field and the error shows up immediately.

Changing the value is the job of the

set()
method. You call it on the signal and give it one argument: the finished new value. The previous contents of the tower carry no weight here, nobody even reads them. This is an order with no discussion attached, of the "it shall be a two, and that is the end of it" variety.

1alertLevel.set(2);
2
3console.log(alertLevel());  // 2

The tower shows a two. The most important part, though, is what that order did not touch:

alertLevel
is still the same signal, the same function, the same thing in memory - I checked this by keeping a reference to it before the write and comparing it afterwards. Only the value inside was exchanged, so every place in the application holding that name will see the new number at once, without having to ask for it. You also did not have to notify anyone of anything: there is no call along the lines of "refresh the view" here, and no event to dispatch. Angular noted the write itself and will itself wake up everyone who was reading from that tower. The method returns nothing, by the way - the result of
set()
is
undefined
, so do not try to assign it anywhere.

Read the statement from left to right, because the order of the pieces never varies: the signal's name, a dot, the word

set
, and at the end a parenthesis with the new value inside. For a tower named
count
and a value of ten that comes out as
count.set(10)
- first
count
, then
.
, then
set
, then
(10)
. My advice, @name: everywhere the new value is simply known - zero after a promotion,
true
after a login, the row picked from a list - reach for
set()
and do not get clever. That order reads in half a second and cannot be misunderstood.

There are orders, however, that cannot be issued without a look at the tower. "Raise the alert by one degree" names no particular number until you know what the tower is showing at this moment. For situations like that a signal has a second method,

update()
, which instead of a finished value takes a function that works the new value out from the old one. You will see it in a moment in the component code, and we will deal with it properly at the next stop - together with the question of which of these two orders is the right one when.

The tower inside a component

Every line so far has been an ordinary constant in a file, so that the mechanics stayed visible without scaffolding around them. In a real application signals are fields of a component class. Let us build a samurai stats panel: it will show the warrior's name, his level and the experience he has earned, and two buttons will let those numbers be changed. We begin with the fields alone, with no decorator and no template.

1export class SamuraiStatsComponent {
2  name = signal('Musashi');
3  level = signal(1);
4  experience = signal(0);
5}

Three towers, and three different things inside them: a string with the name, a number for the level and a number for the experience. There is nothing new in the way they are raised - it is exactly the same

signal()
function as with the alert level, merely called three times and assigned to fields instead of to constants. A signal does not insist on holding a number: a string, a boolean, an object or an array all fit just as well. Notice what this class does not contain: no constructor, no
ngOnInit
method, no list of fields to observe. The lifecycle rituals you met in the second module are of no use here at all, because a signal comes into being together with the class instance and has a value from its very first moment.

The panel is to show one more number: how much experience is missing before the next level. Nobody types that value in by hand, because it follows from the other two - from the level and from the experience earned. In Angular you build it with a third function from the

@angular/core
package, named
computed()
, and we will call our field
xpToNextLevel
. Today you will see it only in passing, and only from the reading side: it is read with parentheses, exactly like any other signal. Where its value comes from, and why nobody ever sets it, is what the whole of the next lesson is about.

1template: `
2  <h2>{{ name() }}</h2>
3  <p>Level: {{ level() }}</p>
4  <p>Experience: {{ experience() }} XP</p>
5  <p>XP to next level: {{ xpToNextLevel() }}</p>
6`

The template reads all four fields in the same way: the name, empty parentheses, the whole thing inside the double braces of interpolation you know from the second module. As far as the template is concerned,

xpToNextLevel()
differs in nothing from
level()
- both lines simply yield a number. Not a single rule of interpolation changed along the way: braces, expression, done. There is also not one line here that would look after refreshing - in a template you do not write down that something is to be redrawn, only what it is to be redrawn from. The only novelty compared with an ordinary field is those two parentheses; had you left them out, a function rather than a number would land inside the braces, and the user would see something on screen that nobody meant to show them.

Two buttons remain. The first adds a hundred experience points, the second raises the level and resets the experience counter. The first method works the new value out from the old one, so the

update()
announced a moment ago falls to it. The second ends with an order you already know precisely:
set(0)
, meaning "it shall be zero, no matter how much there was before the promotion".

1gainExperience(amount: number): void {
2  this.experience.update(xp => xp + amount);
3}
4
5levelUp(): void {
6  this.level.update(lvl => lvl + 1);
7  this.experience.set(0);
8}

Inside a class the signals are fields, so you reach for them through

this
- this is the most frequent slip when copying examples from the documentation, where signals are often plain constants at file level. After a click on the second button the level goes up by one and the experience returns to zero. Nothing changes apart from those two numbers: neither method touches the
name
field, nobody rebuilds the component and nobody orders Angular to refresh the view. Notice as well that
levelUp
issues two separate orders to two separate towers - one tower is one value, and there is no way to change both with a single call.

Let us gather the whole tower together. You know the

@Component
decorator from the first module: the
selector
field is the tag name,
standalone: true
marks the component as self-contained, and
template
carries the template. You also know the
(click)
event binding from the second module. The only new things are the four fields and two methods you assembled separately a moment ago.

1import { Component, signal, computed } from '@angular/core';
2
3@Component({
4  selector: 'app-samurai-stats',
5  standalone: true,
6  template: `
7    <h2>{{ name() }}</h2>
8    <p>Level: {{ level() }}</p>
9    <p>Experience: {{ experience() }} XP</p>
10    <p>XP to next level: {{ xpToNextLevel() }}</p>
11
12    <button (click)="gainExperience(100)">+100 XP</button>
13    <button (click)="levelUp()">Level Up!</button>
14  `
15})
16export class SamuraiStatsComponent {
17  name = signal('Musashi');
18  level = signal(1);
19  experience = signal(0);
20
21  xpToNextLevel = computed(() => this.level() * 1000 - this.experience());
22
23  gainExperience(amount: number): void {
24    this.experience.update(xp => xp + amount);
25  }
26
27  levelUp(): void {
28    this.level.update(lvl => lvl + 1);
29    this.experience.set(0);
30  }
31}

The whole panel is barely thirty lines and you spent none of them on refreshing the screen. The import list from

@angular/core
grew by exactly two names compared with the components from the earliest modules - by
signal
and by
computed
- and that is the entire cost of entry into reactivity. A click on the "+100 XP" button changes one tower,
experience
, and that is enough: Angular knows that two lines of the template are watching it, the one with the XP figure and the one with the experience still missing, so it will attend to exactly those. Nothing changes in the heading with the name or in the line with the level, because neither of them was reading that tower. Notice finally that the class holds not one line guarding consistency - nobody subtracts the experience from the promotion threshold by hand, and yet the last number in the template always adds up.

What you can already do, and what waits beyond the gate

You have everything you need for a first tour of duty on the tower. The

signal()
function raises the tower and gives it a starting value, empty parentheses show what is burning on it at this moment, and the
set()
method issues the order "it shall be this much". That is enough to move every counter, every flag and every form field in your application over to signals, and to leave none of them in need of a messenger walking round all the castles. Three things you cannot do yet, and each has its own stop ahead: working a new value out from the old one along with values that recompute themselves, reacting to a change with something from beyond the screen such as a line in a log, and receiving signals from a parent component.

Remember this, @name: a signal is a tower, not a number - the parentheses show what is burning on it right now, and

set()
is the order after which the smoke goes up without asking any messenger for permission.

Go to CodeWorlds