Learn

Learn

Angular Signals — The Basics

Build a real mental model of Angular signals: what a signal is, reading it as a function call, writing with .set() and .update(), deriving values with computed(), running side effects with effect(), and how the template re-renders when a signal changes.

XCODXLearn · Updated

Introduction

In Angular, a signal is a reactive value: a container that holds some data and knows exactly which parts of your app depend on it. You read a signal by calling it like a function — count() gives you the current value — and you change it through its own methods rather than by assignment. The moment the value inside a signal changes, Angular knows precisely what needs to update, and only that is re-rendered. If you have ever wanted a total to recalculate the instant a quantity changes, that automatic bookkeeping is what a signal gives you.

Why does Angular need this at all? For most of its history Angular tracked changes by re-checking the whole component tree after every event, a strategy that works but asks the framework to guess where a change might have happened. Signals turn that guess into knowledge. Because reading a signal records who read it, Angular builds a dependency graph as your code runs, and that graph is the foundation of fine-grained reactivity — updates that touch only the views a change actually affects, with no manual subscriptions to wire up or tear down.

The whole model is four small tools and one loop. You create state with signal(), read it by calling it, write it with .set() for a fresh value or .update() for a value derived from the old one, derive further values with computed(), and run side effects with effect(). Hold that loop in your head: read a signal, change it through a method, and watch every computed value and template that depended on it recompute. Everything else on this page is a variation on that single cycle, and by the end you will be able to reason about exactly when and why a view updates.

Lessons

1. What a signal is

A signal is a value with a memory of who is watching it. You create one by calling signal() with an initial value, and what you get back is a small function. Calling that function returns the current value; it never returns a promise or an observable, just the value itself, synchronously. The important part is invisible: every time you read a signal inside a reactive context — a template, a computed, or an effect — Angular quietly records that this reader depends on this signal, so it can notify the reader later when the value changes.

import { signal } from "@angular/core";

const count = signal(0); // create a writable signal holding 0
console.log(count());     // read it by CALLING it -> 0

That is the entire read side of a signal: it is a getter you invoke. There is no .value property to reach into and no .get() method — the call itself is the read, and that call is what registers the dependency. Keeping reads as plain function calls is what lets Angular track them without any extra syntax on your part.

2. Writing a signal with set and update

You never assign to a signal directly, because a plain assignment would change a value without telling anyone who depends on it. Instead a writable signal gives you two methods. Call .set() with a brand-new value when the next value does not depend on the current one, and call .update() with a function when it does — the function receives the current value and returns the next one. Both do the same essential job: they store the new value and notify every dependent that it is now stale and must recompute.

const count = signal(0);

count.set(5);              // replace the value outright
count.update((n) => n + 1); // derive the next value from the current one -> 6

Reach for .set() when you have the whole next value in hand, such as writing a fresh string from an input event. Reach for .update() whenever the new value is a function of the old one — incrementing a counter, toggling a boolean, or pushing onto a copy of an array — because it reads the latest value at the moment it runs and stays correct even when several writes happen close together.

3. Deriving values with computed

Most interesting state is derived: a total from line items, a full name from a first and last, a flag that is true only when a form is valid. Angular expresses this with computed(), which takes a function that reads one or more signals and returns a value. The result is itself a read-only signal — you call it to read it — but you never set it, because its value is defined entirely by the signals it reads. When any of those source signals change, the computed marks itself stale and recomputes the next time it is read.

import { signal, computed } from "@angular/core";

const price = signal(10);
const qty = signal(3);
const total = computed(() => price() * qty()); // derives from both signals

console.log(total()); // 30 — recomputes automatically when price or qty change

A computed is also lazy and memoized: it does not run until something reads it, and once it has run it caches its result until a source signal actually changes. Read the same computed three times in a row and its function runs once. That caching is not something you configure — it is how computed behaves by default, and it is why deriving state this way is cheap even when the derivation is used all over a template.

4. Running side effects with effect

Sometimes you need to reach outside the world of values — log something, write to localStorage, update the document title, or call an imperative browser API — whenever a signal changes. That is what effect() is for. You pass it a function, Angular runs it once immediately, tracks every signal it reads, and re-runs it whenever any of those signals change. An effect is the bridge from reactive state back out to the imperative side effects the rest of the world still speaks.

import { signal, effect } from "@angular/core";

const count = signal(0);

effect(() => {
  document.title = "Count: " + count(); // re-runs whenever count changes
});

Use an effect for genuine side effects only. If all you are doing is producing a new value from existing signals, that is a job for computed, not effect — deriving values with a computed keeps them cached and pure, while an effect exists precisely to do the impure work of talking to the outside world.

5. How the template re-renders

Inside a component template you read a signal the same way you do in code, by calling it: {{ count() }}. Because that read happens in a reactive context, Angular records that this specific piece of the view depends on that specific signal. When the signal changes, Angular does not re-check the whole component — it updates only the bindings that actually read the changed signal. This is the payoff of the dependency graph: the loop of read, write, re-render is not just automatic, it is surgical.

@Component({
  selector: "app-root",
  standalone: true,
  template: `<button (click)="count.update((n) => n + 1)">Clicked {{ count() }} times</button>`
})
export class App {
  count = signal(0);
}

Notice that the click handler calls .update() and the template reads count(), and nothing wires the two together by hand. The write notifies the read, the read re-renders, and the button label stays in sync with the value. That is the complete reactive loop expressed in one component.

6. Signals versus older change detection

Before signals, Angular relied on Zone.js to notice that something might have changed and then walked the component tree comparing bindings to find what did. It is a proven model, but it works by checking broadly rather than knowing precisely. Signals invert that: because each read registers a dependency, the framework knows the exact set of views a change affects and can skip everything else. You can adopt signals gradually inside existing components, and over time they let a component update with far less work per change, which is the heart of Angular’s move toward fine-grained reactivity.

That is the full model in one breath: a signal is a reactive value you read by calling it; you write it with .set() or .update(); you derive new values with computed(); you run side effects with effect(); and the template re-renders exactly the bindings that read a changed signal. Everything below lets you feel that loop in a live component.

Practice

Predict first: in the counter below, what does the label show immediately after the first click, and why does it change at all? The answer is that clicking calls .update(), which stores the next value and notifies the template read count(), so Angular re-renders just that binding. Run it and click a few times — this is the whole loop in one small component:

State does not stay isolated. Here a computed derives a doubled value from the same source signal, and the derived line updates on its own every time you change the count. You never set the doubled value — it is defined by what it reads, so changing the source is enough to keep it correct:

Finally, an effect runs impure work when a signal changes. This one writes the current count to the document title and logs it every time you click, showing how a side effect re-runs automatically without any subscription to manage. Open the console as you click to watch it fire:

Next steps

You have the model — now put it to work. Read the signal() reference for the full writable-signal API and the computed() reference for how derived signals memoize. Learn how to set up two-way binding for the most common state task, and browse the runnable Angular signals examples for copy-paste patterns. Curious how another framework tracks changing data? Compare React state. Every snippet here runs in the editor.

Frequently asked questions

What is a signal in Angular?
A signal is a reactive value that holds data and tracks who depends on it. You read it by calling it like a function, such as count(), and you change it with .set() or .update(). When the value changes, Angular updates only the views that read that signal.
How do I read and write an Angular signal?
Read a signal by calling it: count() returns the current value. Write it with count.set(next) to replace the value outright, or count.update((n) => n + 1) to derive the next value from the current one. You never assign to a signal directly.
What is the difference between computed and effect?
A computed derives a new value from other signals and is lazy, cached, and pure — you read it, you never set it. An effect runs a side effect, like logging or writing to the DOM, whenever the signals it reads change. Use computed for values and effect for impure work.
How are signals different from Angular’s older change detection?
The older model used Zone.js to detect that something may have changed and then checked the component tree to find it. Signals record each read as a dependency, so Angular knows the exact views a change affects and updates only those, giving fine-grained reactivity.