Beginner’s Guide to Angular Signals

Luis Ramirez Jr
Luis Ramirez Jr
hero image

If you’ve ever had to juggle @Input, ngOnChanges, or RxJS just to track simple state changes in Angular — and felt like pulling your hair out in frustration — you’re not alone.

The good news? Signals aim to fix that.

Angular’s new reactive primitive gives you a more intuitive way to manage state—without wiring up complex observables or manually managing subscriptions.

In this guide, we’ll break down Angular Signals from the ground up. You’ll see what they are, how they work, and how they can simplify your components. No fluff. No jargon. Just clear explanations, real examples, and practical steps you can use with confidence.

Let’s dive in.

Sidenote: If you want to learn more and improve your Angular skills, then check out my complete Angular Developer course:

learn angular

This is the only Angular course you need to learn Angular, build enterprise-level applications from scratch, and get hired as an Angular Developer. Completely revamped for 2025!

With that out of the way, let’s get into this guide.

What are Angular signals?

Imagine you’re building a counter in Angular. You click a button, and a number on the screen updates. Simple, right?

But behind the scenes, Angular has to figure out what changed and what to update.

Normally, you might use a class property like count = 0, and Angular’s change detection would check the whole component tree every time an event happens, such as a click or a data update.

This works fine for small apps, but it can be inefficient, especially because Angular often ends up checking parts of the app that didn’t actually change. If you want more precise updates, such as re-rendering only when a specific value changes, then you’d need more manual control.

That’s where signals come in.

A signal in Angular is a reactive value. In simple terms, that just means it’s a value that automatically notifies Angular when it changes. Anything that reads from a signal—like your template or a computed value—gets tracked. When the signal updates, Angular knows exactly what to re-run and nothing more.

For example

An easy way to think of this is like a spreadsheet like Excel or Google Sheets. You enter a number in one cell, and another cell that depends on it updates automatically. You don’t have to tell it what changed because it just knows.

That’s how signals work in Angular. When the value changes, Angular updates just the parts of your app that depend on it.

Here’s the basic idea:

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

const count = signal(0);

console.log(count()); // 0

count.set(1);

console.log(count()); // 1

So what's happening in this code?

  • You read the value by calling the signal like a function: count()
  • You update the value with count.set(newValue)

This means:

  • No need to manually trigger change detection
  • No need to use ChangeDetectorRef
  • No subscriptions or unsubscriptions like you’d use with observables

And that in a nutshell is what makes signals powerful because they give you precise, automatic updates with zero boilerplate.

That’s the “what.” But now let’s talk about the “why.” Where did signals come from, and what problems were they designed to solve?

Why Angular added signals (and how they compare to RxJS)

To understand why Angular introduced signals, it helps to zoom out and look at how change detection has traditionally worked.

In the past, Angular relied on something called zone-based change detection.

Basically, whenever anything happened like a click, an HTTP request, or a timer, Angular would re-check the entire component tree to see if anything changed. This worked, but it wasn’t always efficient. Especially in larger apps, this kind of blanket checking could slow things down or trigger updates even when nothing important had changed.

So when developers needed more control, (especially for things like reacting to user input, combining values, or handling async events), they turned to RxJS.

What is RxJS?

RxJS is a separate library for working with data streams i.e. values that change over time. It gives you tools like Observable, which can emit values over time and let you react to them.

For example, you might use an observable to:

  • Track mouse movements
  • Listen for button clicks or form changes
  • Fetch and update data from an API

It’s powerful but it also adds complexity. You have to know when to subscribe, how to use operators like map, switchMap, or combineLatest, and how to clean things up when the observable is no longer needed.

So Angular had two major pain points to solve:

  • Too much boilerplate and complexity, especially for simple state changes
  • Inefficient updates from checking too many components too often

That’s where signals come in.

If you’ve used RxJS before with things like BehaviorSubject or Observable, you might be wondering how signals are different. The key difference is that signals are built into Angular’s change detection system. They’re not streams of events like observables; they’re more like reactive variables that Angular tracks for you.

With signals, Angular can track exactly which parts of your app depend on which values. When a signal changes, only the things that depend on it get re-evaluated. This is called fine-grained reactivity, and it leads to faster updates with less work.

And unlike RxJS, signals are designed to feel like regular variables. There’s no need to subscribe, unsubscribe, or learn a new library. You just call count() to read the value, count.set(1) to change it, and Angular handles the rest.

TL;DR

  • Signals are values. They hold one value at a time and update immediately. If you’re updating a counter, toggling a boolean, or reacting to form input then signals are usually the better fit
  • Observables are streams. They can emit many values over time and are better for async data, user events, or complex flows. If you’re working with async data, background polling, or event-driven logic then observables still make sense

The cool part though is that you don’t have to choose just one. Angular lets you use both, so you get the power of RxJS when you need it and the simplicity of signals when you don’t.

So let’s walk through how to use it.

How to create and use a signal

Creating a signal in Angular is pretty straightforward but there are a few things to understand so it really clicks. Let’s walk through it together like we’re building a small counter.

First, to use signals, you’ll need to import the signal function from @angular/core. This function creates a special reactive value that Angular will keep track of automatically.

Here’s the basic setup:

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

const count = signal(0);

Right there, we’ve created a signal called count and given it an initial value of 0. You can think of this like a smart variable. count holds a number, but it also notifies Angular whenever that number changes.

Now, reading the value is easy. Instead of just using count, you call it like a function:

console.log(count()); // prints 0

That part trips people up at first, because most variables aren’t functions. But the reason we call count() is because this tells Angular, “Hey, this part of the code depends on the signal.”

Angular then uses that information to track what needs to be updated when the value changes.

So, to update a signal, you don’t assign a new value with = like a normal variable. Instead, you call .set():

count.set(1);

console.log(count()); // now prints 1

If you want to update the value based on the current one, such as adding one to a counter, you can use .update():

count.update(current => current + 1);

This is helpful when the next value depends on the previous one, and it keeps your updates safe from timing issues or multiple updates happening close together.

Signals in components

You’re probably wondering, “Cool, but how does this work inside a component?” Great question.

Let’s say you want to show this counter in your template and have a button to increment it. Here’s how that might look in a real Angular component:

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
	<p>The count is: {{ count() }}</p>
	<button (click)="increment()">Add one</button>
  `
})
export class CounterComponent {
  count = signal(0);

  increment() {
	this.count.update(c => c + 1);
  }
}

See how clean that is?

There’s no need for @Input(), no need for ChangeDetectorRef, and no observables to manage. Just a signal holding the state, and Angular takes care of the updates behind the scenes. This is because when count changes, the template re-renders automatically.

This is one of the biggest reasons Angular introduced signals because they make components simpler and easier to reason about, especially when you're managing small pieces of state that change over time.

However, signals don’t just make it easier to store state — they also make it easier to derive new values from that state.

That’s where computed signals come in…

Derived signals and computed values

One of the most powerful parts of working with signals is how easily you can create new values that automatically stay in sync with other signals. These are called computed signals, and they save you from having to write extra logic just to keep your UI or local state updated.

For example

Let’s go back to that counter example, and let’s say that you want to display double the current count.

How would this work?

Well, without signals, you’d probably write a method or calculate the value manually every time. But with signals, you can create a computed value that always reflects the latest result.

Here’s what that looks like:

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

const count = signal(2);
const doubleCount = computed(() => count() * 2);

console.log(doubleCount()); // 4

count.set(5);
console.log(doubleCount()); // 10

Notice what’s happening here:

  • count is a normal signal
  • doubleCount is a computed signal that depends on count
  • Every time count changes, Angular automatically re-runs the function inside computed() to get the new result.

This works because Angular is tracking what the computed signal depends on. So when you call count() inside computed(), Angular records that dependency, and when count changes, doubleCount updates too.

This means you don’t need to call .set() on doubleCount. Heck, you don’t even get to set it. That’s the whole point, because it’s derived from something else, and so Angular keeps it up to date for you.

Handy right?

Why this matters

Computed signals help keep your logic clean and declarative. Instead of scattering updates throughout your code like manually recalculating values when something changes, you describe the relationship once, and Angular handles the rest.

No extra variables. No wiring up listeners. No trying to remember where something was last updated.

They’re especially useful when you’re working with templates. You might compute filtered versions of a list, display summaries, or format user input, all without creating more methods or worrying about when to re-run the logic.

One of the biggest performance wins in Angular’s new reactive system comes from how computed signals handle caching.

When you create a computed signal, Angular automatically tracks the reactive values it depends on. But instead of recalculating the result every time it’s accessed, Angular caches the last computed value and only re-evaluates it if one of the dependencies actually changes.

This means your app isn’t wasting time recalculating values that haven’t changed. It’s especially useful when your logic involves expensive computations or complex UI rendering.

The result? Smoother updates, less unnecessary work, and a more efficient app.

But what if you want to respond to changes, not just calculate values? That’s where effects come in…

Effects and tracking changes

An effect is a block of code that runs automatically whenever one of the signals it depends on changes. Angular keeps track of those dependencies for you, so your effect only re-runs when it needs to — nothing more, nothing less.

For example

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

const count = signal(0);

effect(() => {
  console.log('Count changed to:', count());
});

count.set(1); // Logs: "Count changed to: 1"
count.set(2); // Logs: "Count changed to: 2"

You don’t call the effect yourself. Angular runs it for you the first time, and then only when something it uses changes. In this case, count().

This is really useful for things like:

  • Logging (during development or debugging)
  • Triggering animations when state changes
  • Updating third-party libraries or browser APIs (e.g. localStorage, charts)
  • Performing manual DOM operations when needed

TL;DR

When used the right way, effects help you respond to changes without cluttering your component logic with manual event handling or lifecycle code. They’re a natural fit for situations where reactivity meets the outside world.

So far, we’ve focused on how to use signals in your components. How to create them, update them, and react to changes.

But how does Angular actually know what to update and when? What makes all of this reactivity work so precisely behind the scenes?

Let’s pull back the curtain for a minute and take a quick look under the hood.

How signal reactivity works under the hood

Angular’s signal system is built on something called fine-grained reactivity.

The core idea is that Angular tracks exactly which parts of your code depend on which signals. That way, when a signal updates, Angular only re-runs the specific logic that needs to respond. Nothing more.

Let’s break that down with three key ideas.

Dependency tracking

Whenever you read a signal inside a computed() or an effect(), Angular quietly takes note. It builds a behind-the-scenes map, and this function depends on that signal.

So if your code looks like this:

effect(() => {
  console.log('Theme is now:', theme());
});

Angular tracks that theme() was read inside this effect. Later, when you call theme.set(), Angular knows it only needs to re-run this one effect. No need to re-check the whole component or update anything else.

This is what sets signals apart from Angular’s older change detection model, which often checks entire component trees. Signals bring targeted, precise updates and that’s a huge performance win.

Untracked reads

But sometimes, you don’t want Angular to track a signal read.

Maybe you’re just logging a value at startup, and you don’t want that effect to re-run every time the signal changes. Angular gives you a way to do that using untracked().

For example

import { untracked } from '@angular/core';

effect(() => {
  const initialValue = untracked(() => theme());
  console.log('Initial theme:', initialValue);
});

This tells Angular, “I’m reading this signal, but don’t treat it as a dependency.”

You probably won’t need untracked() often, but when you do, it’s handy for cases like one-time logging or setup code.

Lazy evaluation

One last detail to know is that signals in Angular are lazy.

What I mean by this is that they don’t do anything until something actually reads them. You can define a signal, a computed value, or even an effect, but nothing happens unless it’s used in a template or another reactive context.

This helps avoid unnecessary work and keeps things fast. If no one’s listening, Angular doesn’t bother running the logic.

So, now that you’ve seen how signals work under the hood, you might be wondering how they fit in with the tools you already know — especially observables.

The good news is that you don’t have to pick one or the other. But knowing when to use each makes a big difference.

Signals vs observables: Which one should you use?

Earlier, we looked at how Angular used to rely heavily on RxJS for reactive data and how signals were introduced to simplify common use cases.

So now that Angular supports both signals and observables, the question is: when should you use which?

The answer depends on the kind of data you're working with.

Use signals for synchronous, local state

Signals shine when you're dealing with values that live inside a component and change over time.

Things like:

  • A counter
  • A selected tab
  • A boolean toggle
  • A filtered list
  • A form input

These are all synchronous, local values that your component owns. You don’t need streams, operators, or subscriptions. You just want to read and update a value and let Angular handle the rest.

That’s exactly what signals give you. They’re simpler, more ergonomic, and fully integrated into Angular’s change detection system. You read the value with count(), update it with count.set(1), and Angular updates the UI automatically. No boilerplate needed.

Use observables for asynchronous or event-based data

Now let’s say you’re calling an API, reacting to mouse movements, or dealing with a live WebSocket connection. These are things that emit multiple values over time and that’s where observables are still the right choice.

Observables let you handle:

  • Streams of HTTP responses
  • Button clicks or input events
  • Drag-and-drop interactions
  • Intervals, timers, or delays
  • Debounced or combined streams

They give you access to powerful operators like map, filter, switchMap, and combineLatest, which are perfect for handling complex async flows or coordinating multiple event sources.

So how do you decide which to use?

Here’s a simple way to think about it:

  • If you’re storing component state, use a signal
  • If you’re reacting to external data or events, use an observable

The best part of course is that you don’t have to choose just one, because Angular makes it easy to move between the two with utilities like toSignal() and fromSignal().

For example

Let’s say you’re loading a user profile from an API.

The HTTP call is asynchronous, so you’d use an observable for the request. But once the data arrives, you want to work with it reactively inside your component, so you convert it into a signal.

import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';

@Component({...})
export class UserComponent {
  user$ = this.http.get('/api/user');
  user = toSignal(this.user$); // Now it's a signal

  constructor(private http: HttpClient) {}
}

Now you can use user() directly in your template or pass it into a computed signal with no need for async pipes or manual subscriptions.

Super handy!

However, before you start adding signals everywhere, let’s cover a few common mistakes and how to avoid them.

Common mistakes with signals and how to avoid them

Signals simplify a lot of what used to be complex in Angular, but like anything new, they come with their own set of gotchas and these mistakes are easy to make, especially if you're used to observables or just starting to learn reactive programming.

The good news is they’re also easy to fix once you understand what’s going on, so let’s walk through the most common ones you’re likely to run into.

Syntax gotchas

Forgetting to call the signal when reading its value

This is the most common one, especially when you're just getting used to the syntax. You might write something like:

<p>{{ count }}</p> <!-- ❌ This won't work -->

But signals aren’t regular variables. You have to call them like functions to get their value:

<p>{{ count() }}</p> <!-- ✅ This works -->

This means that if you forget the (), Angular won’t know that the template depends on that signal, and the view won’t update when it changes.

Using the wrong tool

Using signals where a computed value makes more sense

Sometimes, people create a signal, manually update it inside an effect, and end up recreating logic that computed() was designed for.

// ❌ Not ideal

const count = signal(2);

const doubled = signal(0);

effect(() => {

  doubled.set(count() * 2);

});

This works, but it’s not the right tool. You’re using an effect to do what a computed signal is built to handle.

Here’s the better version:

// ✅ Clean and reactive

const count = signal(2);

const doubled = computed(() => count() * 2);

Basically, use computed() when you’re deriving one signal from another as it’s simpler, safer, and automatically stays in sync.

Overusing effects for everything

Effects are great for side effects such as logging, syncing to localStorage, or triggering an animation. But they’re not meant to replace your app’s business logic.

A good mental model is this:

  • If you need to calculate a new value → use computed()
  • If you need to trigger a behavior → use effect()

Try not to cram too much into your effects. If you're reaching for .set() inside an effect just to update another signal, that’s a sign you might be using the wrong pattern.

Update methods confusion

Confusing set() with update()

Both methods update a signal, but they do it in different ways.

  • set(newValue) is for when you already know the new value
  • update(fn) is for when the new value depends on the current one

If you’re incrementing a count or toggling a boolean, update() is usually the better choice:

count.update(current => current + 1);

It avoids timing issues where multiple updates happen close together.

Trying to update a computed signal directly

Another important detail to keep in mind is that computed signals are read-only, and that’s by design.

A computed signal is meant to be a derived value, one that automatically updates based on the signals it depends on. That means you can read its value like a normal signal, but you can’t modify it directly.

Trying to do something like myComputedSignal.set(...) will either throw an error or simply do nothing. Angular prevents this because computed signals are supposed to reflect other values, not store state themselves.

So if you ever want to change the result of a computed signal, the correct approach is to update the underlying signal it depends on. Angular will then recalculate the computed value for you automatically.

For example

Here’s an example of what not to do:

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

const count = signal(0);

// Computed signal based on `count`

const doubleCount = computed(() => count() * 2);

// ❌ This will throw an error or simply not work

doubleCount.set(10); // Error: computed signals are read-only

This mistake is easy to make if you’re used to writable variables everywhere. But once you understand that computed signals are meant to be reactive formulas, not standalone state, it becomes second nature.

Interop edge cases

Not unsubscribing from observables when converting to signals

This one’s specific to toSignal(). If you pass an observable that never completes (like interval() or fromEvent()), make sure you use the options argument to clean it up or you’ll create a memory leak.

For example

import { interval } from 'rxjs';

import { toSignal } from '@angular/core/rxjs-interop';

const seconds$ = interval(1000);

const seconds = toSignal(seconds$, { requireSync: true });

In practice, this only comes up in more advanced cases, but it’s good to know that signals converted from observables don’t magically manage subscriptions unless you configure them to.

Tl;DR

Honestly, most of these mistakes come from forgetting how signals differ from regular variables or observables. Once you start thinking in terms of reactive relationships, not just values, they become second nature.

Give signals a try in your own Angular projects!

So as you can see, signals give you a cleaner, more reactive way to manage state in Angular but without all the boilerplate. Instead of wiring up subscriptions or triggering change detection manually, you just describe how things relate, and Angular updates them for you.

You can still use @Input(), observables, and the tools you already know and love, but signals make it easier to manage local state with less code.

Try it out. Start small, update a toggle or counter with a signal, and see how much simpler it feels.

P.S.

Remember, if you struggle with any of the concepts in this guide, or want to take a deep dive into Angular, check out my complete Angular Developer course:

learn angular

This is the only Angular course you need to learn Angular, build enterprise-level applications from scratch, and get hired as an Angular Developer.

Want a sneak peak?

I went ahead and uploaded the first 4 hours of the course below for free:


Plus, once you join, you'll have the opportunity to ask questions in our private Discord community from me, other students and working professionals.


Not only that, but as a member - you’ll also get access to every course in the Zero To Mastery library!

Check out more Angular content!

Check out some of my other Angular articles, guides and cheatsheets!

More from Zero To Mastery

Getting Started With Animations For Angular: Fade + FadeSlide preview
Getting Started With Animations For Angular: Fade + FadeSlide
15 min read

Do you want to improve the user experience for your angular app? Learn how to set up simple animations today!

Top 10 Angular Projects For Beginners And Beyond preview
Popular
Top 10 Angular Projects For Beginners And Beyond
9 min read

Are you looking to skill up in Angular? We give you 10 Angular practice projects to make that happen plus our Top 3 you can't miss for you to up your game!

Top 30 JavaScript Interview Questions + Answers (with Code Snippets) preview
Top 30 JavaScript Interview Questions + Answers (with Code Snippets)
35 min read

Are you a Developer looking to ace your front-end interview? Here are 30 of the most common JavaScript interview questions (w/ answers + code examples) to help you know if you're ready or not!