@name, at the last stop you lit your first smoke signal. You called
signal(), handed it a starting value and read it back with a pair of parentheses. The tower stands, the smoke rises, the neighbouring castle is watching. The trouble is that a tower which can only ever light itself stops being useful to anyone after the first minute of observation. A tower earns its keep the moment the guard has to change something in the smoke.Look once more at the samurai stats component from the previous lesson. Two calls slipped past you there and nobody explained them:
this.experience.update(xp => xp + amount) when experience is gained, and this.experience.set(0) on promotion. Two different names, two different orders - and they are not interchangeable. There was also a field called xpToNextLevel that nobody ever assigned to, and which nevertheless always showed the right number. Three puzzles, one lesson.Master Ango-san puts it like this. A row of towers stands along the Academy walls, and the commander can send the guard at each of them only three kinds of order. The first is "light exactly three columns of smoke", and the guard does not even need to know how many were burning a moment ago. The second is "add one column to whatever is already showing" - here the guard has to look at his own tower first. The third order never reaches a guard at all; it goes to the scribe sitting in the command tower: "add up the smoke from towers one and two and post the result on the board". The scribe lights nothing, and his board is never out of date. In code these three orders are called
set(), update() and computed(), and by the end of this lesson you will know how to issue each of them.Before you can issue any order you need a tower. You import the
signal() function from the @angular/core package, hand it a starting value, and get back a writable signal - in the Angular documentation its type is called WritableSignal, literally "a signal you are allowed to write to". Reading looks exactly the way you learned it before: the name and an empty pair of parentheses, because a signal is a function.1import { signal } from '@angular/core';
2
3const count = signal(0);
4
5console.log(count()); // 0A tower now stands with a zero at the top, and that is all that has happened so far. It is worth knowing straight away what reading does not do: it does not change the value, it does not make a copy and it consumes nothing. You can call
count() a hundred times in a row and get the same number every time. Notice too the type, which TypeScript inferred on its own from the starting value. signal(0) is a signal of a number, so trying to write a string into it ends with a compilation error before the application even starts. That is the same discipline of types you met in the first module with the Bushido code of programming, only tucked away behind a single function call.Now the first order. The
set() method takes one argument - a finished new value - and puts it in place of the old one. The previous contents of the signal count for nothing here; nobody even reads them. This is an order with no discussion attached: "it shall be ten, and that is the end of it".1count.set(10);
2
3console.log(count()); // 10The tower shows ten. The important part, though, is what the order did not touch:
count is still the same signal, the same function, the same thing in memory. Only the value inside it was exchanged, so every place in the application holding a reference to count sees the new number immediately, without asking for it. You also did not have to tell anyone that something had changed - Angular noted the write itself and will wake up whoever depends on that tower.Some orders, though, cannot be issued without looking at the tower. "Add one column of smoke" names no particular number until you know how many are already burning. Yes, you can work around it with
count.set(count() + 1), meaning "read, add, set" - but that is three actions glued into one line, and the intention drowns in the parentheses.That is what
update() is for. This method takes not a value but a function. Angular calls it for you, passing in the current contents of the signal, and whatever your function returns is stored as the new value. The name of the parameter is yours to choose: it can be v, current, xp or lvl - the compiler has no opinion here.1count.update(v => v + 1);
2
3console.log(count()); // 11Ten became eleven, even though the number eleven appears nowhere. Read that call from left to right, because the order of the pieces never varies: first
count.update(, then the parameter name v, then the arrow =>, then the expression v + 1 that builds the new value, and finally the closing ). Nothing changed apart from the number itself: the signal is the same one as a moment ago, and the function you passed in was called exactly once and will never be needed again.Now that you know both orders, let us settle the difference for good, because three untrue stories circulate about it. There is exactly one difference and it concerns where the new value comes from:
set() receives it ready-made from you, update() works it out from the previous one using the function you supplied. It is not a matter of speed - both methods end in exactly the same thing, one value written and the dependants notified, so the tale of "a fast set() and a slow update()" has nothing to stand on. It is not a split by data type either: both methods work with numbers, strings, objects and arrays alike, so "set() for objects, update() for primitives" is invented from beginning to end. And they are certainly not two aliases for one method - if they were, they would take the same argument, and they take completely different ones: one a value, the other a function. My advice, @name: wherever the new value follows from the old one, reach for update(). count.update(v => v + 1) says "increase by one" outright, while count.set(count() + 1) makes the reader decode it from three separate steps.Not every signal can be filled with a sensible value straight away. The scout has only just set off into the mountains and for the first few hours there simply is no report - and yet the component has to exist already and show something. What you need is a signal that is allowed to be empty.
Let us first agree on what a samurai is in our code. It is an object with three fields:
name of type string, level of type number and skills, an array of strings naming the martial arts he has mastered. A shape like that is described by an interface - the TypeScript construct you met in the first module. We will call it Samurai and it will serve us to the end of the lesson. The starting value of the signal will be null, meaning "nothing is known yet", and the allowed type is written Samurai | null, where the vertical bar reads as "or". You hand that type to signal in angle brackets placed directly after its name, before the round ones.1interface Samurai {
2 name: string;
3 level: number;
4 skills: string[];
5}
6
7const scout = signal<Samurai | null>(null);The angle brackets after
signal are a type parameter, and this time they are compulsory. Without them TypeScript would infer from the starting value that this is a signal of null alone, and would reject every later attempt to put a samurai in there. Apart from that one detail nothing changes: scout.set(...) and scout.update(...) behave exactly as they did with the counter, and reading scout() gives back a value of type Samurai | null, so before reaching for the name field you have to check whether the report has arrived at all.Now picture a tower manned by a single guard but watched by half the castle. The heralds are there to read the smoke and carry the news; they have no right to add columns of their own. This arrangement shows up constantly in code: a service or a component keeps a signal to itself, changes it in its own methods, and wants to hand it to everyone else in a form nobody outside can quietly reset.
The method for that is
asReadonly(). You call it with no arguments at all and get back a signal of type Signal rather than WritableSignal - one that no longer has either set() or update() on it.1const readonlyCount = count.asReadonly();
2
3console.log(readonlyCount()); // 11Reading gives the same eleven as a moment ago, because this is not a copy and not a frozen snapshot. It is a window looking out onto the same tower. You can see that best when you change the original: the read-only version shows the new value immediately, even though it has no means of changing it itself.
1count.set(99);
2
3console.log(readonlyCount()); // 99
4
5// readonlyCount.set(0); <- compile error: this method does not existI checked this on a running signal: after writing to
count, reading through readonlyCount gives ninety-nine, and the set and update properties are simply not on that object - they come back undefined. Nothing changed in the value itself or in the original signal along the way: count may still be written to, because the right to issue orders is taken away only from the name you hand out to the world.Three other names that sound just as reasonable do not exist, and it is worth knowing why. There is no
freeze() in the signal interface - plain JavaScript does have Object.freeze, but that is a different matter entirely: it freezes the properties of an ordinary object and knows nothing about Angular reactivity. There is no lock() either, tempting as the name is. The third one is the sneakiest: const(). The word const really does exist in JavaScript, only it is a declaration keyword and not a method. Notice that your counter was born as const count = signal(0) - and despite that const you called set(99) on it a moment ago without the slightest resistance. const protects only the binding between the name and the signal, never the value the signal holds inside.So far the tower has held a number. Far more often it holds a whole object - a samurai's card, a player's settings, a list of orders. Let us use the
Samurai interface we agreed on earlier and build a signal with a real warrior inside.1const samurai = signal<Samurai>({
2 name: 'Musashi',
3 level: 1,
4 skills: ['Kendo']
5});Musashi is at level one with a single martial art mastered. Nothing new happened here - this is still the same
signal() as with the counter, just with an object instead of a number. The trouble starts at the first attempt at a promotion, because instinct suggests a solution that looks innocent and is completely wrong.1// DO NOT DO THIS - the signal will not notice
2samurai().level = 2;I checked this on a running signal and the result is worse than it may seem. The field really does change - reading
samurai().level gives two - but nobody who depends on it knows. A derived value computed from that signal kept returning the old number and did not recalculate even once. The reason is simple: Angular decides whether a signal has a new value by comparing it with the old one by identity, and you swapped a field inside the object without handing over a new object. As far as the signal is concerned it is still the same thing at the same address, so there is nothing to report. This is the worst kind of bug there is, because the application throws no exception - the screen just shows stale data.In the Academy we would call the remedy rewriting the scroll. You do not cross a line out in the old document; you write out a new one, carrying over everything that stays the same and entering the one field your own way. In JavaScript the tool for that is the spread operator, written as three dots:
...s puts every field of the old object into the new one. A field named after it overrides whatever came in from the copy.1samurai.update(s => ({
2 ...s,
3 level: s.level + 1
4}));The samurai is at level two, and this time the signal knows about the promotion, because it was handed a brand new object. Treat the line from the previous block as though it never ran - it was a demonstration of a mistake, not a step in the programme - so this update starts at level one and lands on two. Note the round brackets around the brace: without them JavaScript would read
{ as the start of a function body rather than an object literal, and the whole thing would make no sense. The most important part, though, is what stayed untouched: nobody rewrote name or skills by hand, they travelled across on the three dots, and the previous object still exists and still has level one - I checked that by keeping a reference to it before the update. The new value does not destroy the old one; it comes into being beside it.The same rule holds for arrays, only the three dots move inside square brackets.
[...s.skills, 'Iaido'] builds a new array: first all the existing skills, then the added one at the end. The block below is four lines, and in your editor you will see them one under another always in this order - the opening of the update, the copy of the fields, the new contents of the array, the closing.1samurai.update(s => ({
2 ...s,
3 skills: [...s.skills, 'Iaido']
4}));Musashi now has two martial arts: Kendo and Iaido. Neither
name nor level changed along the way - both went through ...s untouched, and the old array holding Kendo alone still lies undisturbed in the previous object. Instinct wants you to write s.skills.push('Iaido') here, and that is exactly the same trap as a moment ago: push appends to the existing array in place, so the object stays the same one and the signal again reports nothing.That leaves the most interesting order, the one given to the scribe. Picture a simple invoice: a unit price, a number of items, a tax rate and a total to pay. The net amount, the tax and the total can all be worked out from the first three numbers, so keeping them in separate writable signals would be asking for trouble - after every change of price you would have to remember three
set() calls in the right order. One day you will forget one, and the board on the wall will start lying.You import
computed() from the same @angular/core package. You give it a function with no arguments and it returns a signal whose value you never set yourself - Angular works it out by calling your function. While doing so it remembers which signals were read inside, and treats them as its dependencies. Let us start with three ordinary towers for the scribe to read from.1import { computed } from '@angular/core';
2
3const price = signal(100);
4const quantity = signal(2);
5const taxRate = signal(0.23);Those are three writable signals and nothing more - none of them watches anyone, each just holds its own number. All the bookkeeping is still to come, and it comes in three steps, because derived values may be built out of other derived values too. The partial sum
subtotal comes from the price and the quantity. The tax tax is worked out from the partial sum and the rate. The whole total is the partial sum increased by the tax. Write them in exactly this order - the one the others use first - because that way they read like a sentence and you never refer to a constant that does not exist yet.1const subtotal = computed(() => price() * quantity());
2const tax = computed(() => subtotal() * taxRate());
3const total = computed(() => subtotal() + tax());
4
5console.log(total()); // 246I ran this code and it printed two hundred and forty-six. The arithmetic is transparent: a hundred times two is two hundred, two hundred times twenty-three hundredths is forty-six, and two hundred plus forty-six is two hundred and forty-six. Inside each of those functions you read the signals the way you always do, through parentheses -
price(), quantity(), subtotal(). Notice also that you nowhere called subtotal.set(...): no such method exists on a derived value and there is no point looking for it.And now the interesting part. Change one single thing, the price, and read the total again.
1price.set(200);
2
3console.log(total()); // 492It printed four hundred and ninety-two, and that is a real number, checked on running code: two hundred times two is four hundred, four hundred times the rate is ninety-two, together four hundred and ninety-two. Pay attention to what you did not do, because this is the most important sentence in the lesson. You did not rewrite a single one of the three definitions. You did not call any "recalculate". You did not set
subtotal, tax or total anywhere in the programme. You changed a tower and the scribe corrected the board himself, because he remembered what he computes it from. That is exactly how xpToNextLevel worked in the component from the previous lesson.So let us pin down precisely what a derived value is, because three other sentences like to pose as the definition. Yes,
computed() returns a read-only signal - but "read-only" on its own explains nothing, because asReadonly() returns a read-only signal too and recalculates nothing. Yes, a derived value is efficient: I checked it, and the function does not run even once until somebody reads the result, while three reads in a row call it only once because Angular remembers the last result; writing the same value to a source does not wake the dependants at all. That, however, is a side effect and not a definition - "a signal optimised for performance" describes anything and nothing. Nor is it about mathematics of any kind: a derived value returns a string, a condition or a filtered list just as happily.1const battleReady = computed(() => samurai().skills.length >= 2);
2const label = computed(() => samurai().name + ' (level ' + samurai().level + ')');The first of them returns true, because Musashi has mastered two martial arts; the second returns the string "Musashi (level 2)". I checked both on running code. Neither computes anything mathematical, and both are perfectly proper derived values, because there is only one definition: a signal whose value Angular recalculates automatically whenever any of its dependencies changes. If you needed the doubled price,
computed(() => price() * 2) would be enough - the same pattern, five pieces in a fixed order: the name computed, the opening bracket, the argument-less arrow, the expression, the closing bracket.Every example so far has been a plain constant at file level, so that the mechanics were visible without any scaffolding around them. In a real application those same signals are fields of a component class, so you read them through
this - and, importantly, inside the function handed to computed() as well, because the same object applies in there. That is the most common slip when moving examples from the documentation into your own component.The rest of the code below you know from the first modules: the
@Component decorator with a selector field, which is the tag name, with a standalone field marking a self-contained component, and with a template field where values go inside double braces. Only the three class fields are new.1import { Component, signal, computed } from '@angular/core';
2
3@Component({
4 selector: 'app-tower-ledger',
5 standalone: true,
6 template: `
7 <p>Quantity: {{ quantity() }}</p>
8 <p>Price: {{ price() }}</p>
9 <p>Subtotal: {{ subtotal() }}</p>
10 `
11})
12export class TowerLedgerComponent {
13 price = signal(100);
14 quantity = signal(2);
15
16 subtotal = computed(() => this.price() * this.quantity());
17}The template reads all three signals in the same way, through parentheses, and cannot tell a derived value from an ordinary one - as far as it is concerned
subtotal() is simply a number. The class gained no new method along the way: there is no ngOnInit, no manual refresh, nothing beyond three fields. When you call this.price.set(120) in any method, Angular recalculates subtotal and refreshes only the fragment of the view that uses it. The decorator looks exactly as you met it in the first module with the independent warriors - a selector, the mark of a self-contained component and a template.Remember, @name:
set() is the order "light exactly this much smoke", update() is "add to what is already showing", asReadonly() is a window for the heralds, and computed() is the scribe who reads the towers himself and corrects the board himself - and at the next stop you will find out what to do when someone from beyond the Academy walls has to react to a change in the smoke.