@Component({
  selector: 'app-child',
  standalone: true,
  imports: [CommonModule],
  template: `
    <h1>Hello</h1>
  `,
})
export class ChildComponent implements OnChanges {
  @Input()
  public changed = false;

  private parent = inject(ParentComponent);

  public ngOnChanges() {
    if (this.changed) {
      this.parent.text = 'from child';
    }
  }
}

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [ChildComponent],
  template: `
  {{text}}
    <app-child [changed]='true'/>
  `,
})
export class ParentComponent {
  text = 'from parent';
}
ERROR
Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'from parent'. Current value: 'from child'. Find more at https://angular.io/errors/NG0100

Signals: A High-Level Overview

To understand signals, it helps to look at how Solid—a newer framework gaining traction, built entirely around this concept—defines them. The Angular team collaborated closely with Solid's creator, Ryan Carniato, to shape their own implementation:

Signals are the cornerstone of reactivity in Solid. They contain values that change over time; when you change a signal's value, it automatically updates anything that uses it.

The idea itself is straightforward. A signal wraps a single value, keeps track of everything that reads it, and pushes updates to those consumers the moment the value changes.

For developers coming from RxJs, a common mental model is to think of signals as BehaviorSubjects minus the manual subscription management. You don't need to subscribe or unsubscribe—signals handle that wiring automatically.

Signals always hold a value. They have no side effects on their own. And they are reactive: whenever the underlying value changes, every dependent recalculates and stays in sync.

Together, these properties introduce a clear, consistent model for how state flows through an Angular application—a model that today's framework does not really provide. Let's unpack that claim.

Where This Journey Began: A Decade Ago

Angular's path to signals actually started about ten years back. That may sound surprising, given that Solid—the framework credited with popularizing signals—only shipped its first stable release (v1.0.0) on June 28th, 2021. How could Angular have been heading in that direction a decade earlier?

The answer lies in Angular's initial v1.0.0 release on June 13th, 2012. That release brought with it a set of architectural decisions, each with its own trade-offs and far-reaching effects on both developers and end users.

Those choices were made within the constraints of their era—no arrow functions until ES6 landed in 2015, no async functions before ES8 in 2017—and based on the browser landscape of the time. After ten years of evolving browser technology and millions of production applications running Angular, the team decided to revisit those original design decisions and evaluate how well they actually held up. As is often the case, some decisions aged better than others.

One of the strongest early calls was choosing TypeScript. It looks obvious in hindsight, but back in 2012 TypeScript was still gaining traction; many frontend developers had never worked with static type safety and saw it as unnecessary overhead. Angular broke new ground by being the first major framework built on TypeScript. Today it's widely accepted that type safety significantly improves both developer confidence and velocity at scale. But that's a side story.

Another foundational decision was that view state could be created and modified anywhere in the application. A simple boolean in a component, or an element deep inside an object held by a global service—Angular would detect changes to any of it and update the DOM. This design grants developers remarkable freedom in how they structure applications. Complex logic can be lifted into services with minimal effort, without ever worrying about how new data reaches the DOM. Whatever you change and wherever you put it, Angular's built-in change detection scans the app, figures out what's different, and updates the view automatically.

The Price of Automatic Change Detection

That convenience, however, comes with some real costs.

ZoneJs and the Trigger Problem

The automatic system relies on ZoneJs, a library that patches browser APIs and tells the framework when anything potentially significant happens. The first trade-off shows up right there: before an Angular app can do anything, ZoneJs must load and execute. That puts Angular at a structural performance disadvantage compared to frameworks using different synchronization strategies for model and view.

What counts as a significant event? Event listeners, setTimeout, Promise resolutions, and more. When state can genuinely live anywhere and change via any of those hooks, nearly any browser event counts as significant. So even when nothing in the component tree should have changed, Angular will still check every binding in every component to see if any value shifted. The result is double-edged. On one hand, Angular tends to over-check, running unnecessary work while hunting for model changes. On the other hand, the algorithm deciding which bindings are stale brings its own complications.

Unidirectional Flow and the Famous Error

Given the sheer frequency of change-detection triggers, the underlying mechanism must be exceptionally fast for applications with many components. That's exactly why Angular runs change detection in DOM order, checking each model-to-DOM binding only once per cycle. After decades—well, years—of accumulated experience with this approach, its weaknesses have become clear. The most significant limitation: you cannot update any parent data after its value has already been checked within the same cycle. That restriction is what leads straight to the infamous ExpressionChangedAfterItHasBeenCheckedError.

There's a Stackblitz showing exactly this failure mode in practice.

In that example, during a single change-detection pass, the component goes against the natural top-down flow by mutating a parent property after Angular has already stabilized it. The framework notices the mismatch and throws.

The example might feel contrived, but there are numerous real scenarios where application data flow simply doesn't follow that strict top-to-bottom pattern. Even Angular's own internals hit this: in the FormsModule, a child's validity dictates the parent's. To update that parent status without triggering the error, the framework wraps the operation in an immediately-resolved promise. That schedules a microtask; once it runs, Angular performs another change-detection pass, this time traversing the tree in the allowed order and updating the DOM accordingly.

This hack—or rather, this workaround—illustrates the point. There are ways to bend change detection into shape, but they're unintuitive and demand a deeper grasp of ZoneJs internals. The vision of automatic global change detection that "just works" all the time hasn't fully materialized in practice.

OnPush, RxJs, and Why It Falls Short of Solving Our (Diamond) Problems

Before we dive deeper, if the concepts of OnPush change detection or RxJs are not completely clear, I highly recommend watching this excellent video by Joshua Morony. It covers both of these critical Angular topics in a very approachable way.

OnPush & the Async Pipe: Treating Symptoms Instead of Curing the Disease

As we've established, Angular's default change detection runs frequently. In small apps this is rarely a problem, but as the number of components and DOM bindings grows, performance becomes a primary concern.

A widely adopted performance strategy is to switch components to the OnPush change detection strategy and offload subscription handling to Angular's AsyncPipe.

Applying OnPush to a component exempts it, along with its entire subtree, from the standard change detection cycle. For a deep dive into the mechanics, this write-up by Angular University is a must-read.

It cannot be overstated: OnPush changes the change detection behavior for that component and every child beneath it.

The official documentation puts it this way:
Use the CheckOnce strategy, meaning that automatic change detection is deactivated until reactivated by setting the strategy to Default (CheckAlways). Change detection can still be explicitly invoked. This strategy applies to all child directives and cannot be overridden.

The ripple effect here is substantial. If a single component high in the tree adopts OnPush, then every component below it effectively needs to be OnPush-compatible as well. This places a significant burden on UI library authors, who must ensure their components work under OnPush so that they can be used across the broader Angular ecosystem.

RxJs: Declarative and Reactive Power for Asynchronous Streams

RxJS is a library for reactive programming using Observables, to make it easier to compose asynchronous or callback-based code.

Mike Pearson's tweet below captures its power brilliantly, and it's a sentiment I fully agree with:

RxJs makes it possible to build declarative, reactive pipelines from Observables. In a single flow, you can clearly see the sequence of operations and state mutations that will occur in an asynchronous process. This paradigm helps eliminate race conditions and results in code that is more legible and easier to reason with.

For many of us, Angular was our gateway into RxJs. The framework makes extensive use of it internally. The most prominent example is the HttpClient, which exposes the HTTP response as an Observable. Furthermore, every Angular FormControl provides a property called valueChanges. This is a multicasting observable that emits an event every time the value of the control changes, in the UI or programmatically.

When combined with OnPush and the AsyncPipe—which binds an Observable directly to the template—these reactive pipelines have become the go-to pattern for creating performant, declarative Angular apps that are free from race conditions.

Streams Are Not Behaviors: Why RxJs Is Not the Answer

But upon closer inspection of how Angular actually utilizes Observables, a clear pattern emerges. Angular uses RxJs for events—specifically, for exposing streams of events. These streams do not hold a current value. Alex Rickabaugh offers a brilliant way to think about this in their conversation with Ryan Carniato.

He illustrates this with click events.

You cannot ask, "What is the current click event?" The question is nonsensical. You could ask, "What was the most recent click event?"—but that's fundamentally a different inquiry. Behaviors, on the other hand—and this is where Angular's signals fit in—always possess a value. You can always ask, "What is this behavior's (signal's) current value?" This foundational gap is the core reason RxJs Observables are not the solution to the problems the Angular team aims to solve with their new reactive primitive.

The team acknowledges in the same discussion that RxJs's BehaviorSubject is closest to a signal. It always holds a value, it can notify subscribers of changes, and it offers methods to retrieve and set the current value. However, it's poorly integrated within the RxJs ecosystem. The moment you pipe it through an operator like map(), it morphs into a plain Observable and loses its guarantee of a current value. Moreover, RxJs is packed with a vast range of powerful operators for mapping, joining, debouncing, and more. For newcomers, this power introduces a daunting learning curve. The Angular team sought something more streamlined as the foundation for their reactive primitive.

Choosing the Right Tool for Your (Diamond) Problems

Let's also recall the original objective. The team wanted to build a reactive primitive that integrates with Angular's template engine, informing the framework when a bound value changes so it can update the DOM accordingly.

A critical aspect of such a primitive is glitch-free execution. This means ensuring that user code never observes an inconsistent intermediate state where only a portion of the connected reactive sources has been updated. When you run a reactive computation, every upstream source must already be finalized. This concept is beautifully explained in Milo's article on fine-grained reactive performance.

Once again, the RxJs solution feels cumbersome and contrived. Consider the following example:

@Component({
  selector: 'normal',
  standalone: true,
  imports: [CommonModule],
  template: `
    <p>Hello from {{fullName$ | async}}!</p>
    <p>{{fullNameCounter}}</p>

    <button (click)="changeName()">Change Name</button>
  `,
})
export class NormalComponent {
  public firstName = new BehaviorSubject('Peter');
  public lastName = new BehaviorSubject('Parker');

  public fullNameCounter = 0;

  public fullName$ = combineLatest([this.firstName, this.lastName]).pipe(
    tap(() => {
      this.fullNameCounter++;
    }),
    map(([firstName, lastName]) => `${firstName} ${lastName}`)
  );

  public changeName() {
    this.firstName.next('Spider');
    this.lastName.next('Man');
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. We create two BehaviorSubject instances for firstName and lastName.
  2. We merge them into an Observable named fullName$, which simply concatenates the two values.
  3. We introduce a fullNameCounter to track how many times fullName$ emits.
  4. We define a changeName function to set both firstName and lastName to new values SIMULTANEOUSLY.

The initial render of our component looks like this:

Hello from Peter Parker!

1
Enter fullscreen mode Exit fullscreen mode

When you click the button, the UI updates to:

Hello from Spider Man!

3
Enter fullscreen mode Exit fullscreen mode

Notice that our fullName$ Observable emitted twice—once for the change to firstName and once for lastName. Even though this happened too quickly to see, the component did render an intermediate state, instantly replaced by the final correct one.

To achieve the desired glitch-free behavior, we'd need to introduce a debounceTime to coalesce those rapid emissions into a single update.

@Component({
  selector: 'debounced',
  standalone: true,
  imports: [CommonModule],
  template: `
    <p>Hello from {{fullName$ | async}}!</p>
    <p>{{fullNameCounter}}</p>

    <button (click)="changeName()">Change Name</button>
  `,
})
export class DebouncedComponent {
  public firstName = new BehaviorSubject('Peter');
  public lastName = new BehaviorSubject('Parker');

  public fullNameCounter = 0;

  public fullName$ = combineLatest([this.firstName, this.lastName]).pipe(
    debounceTime(0),
    tap(() => {
      this.fullNameCounter++;
    }),
    map(([firstName, lastName]) => `${firstName} ${lastName}`)
  );

  public changeName() {
    this.firstName.next('Debounced Spider');
    this.lastName.next('Man');
  }
}
Enter fullscreen mode Exit fullscreen mode

After adding this operator, the counter increments by one, confirming that we no longer render a throwaway intermediate state.

Hello from Peter Parker!

1
Enter fullscreen mode Exit fullscreen mode

Now when you click the button, the display appears like this:

Hello from Debounced Spider Man!

2
Enter fullscreen mode Exit fullscreen mode

I strongly suggest you play with the working example to see this behavior for yourself.

A key insight here is that the behavior of combineLatest—emitting for every update to any of its source observables—would equally apply if Angular decided to turn @Input()s into Observables. Despite being one of the community's most-wanted features, this reveals the hidden complexity that such a design would introduce.

So, what does this same functionality look like when implemented with a signal primitive? I'm pleased to show you.

@Component({
  selector: 'my-app',
  standalone: true,
  template: `
    <p>{{ fullName() }}</p>
    <p>{{signalCounter}}</p>
    <button (click)="changeName()">Increase</button>
  `,
})
export class App {
  firstName = signal('Peter');
  lastName = signal('Parker');

  signalCounter = 0;

  fullName = computed(() => {
    this.signalCounter++;
    console.log('signal name change');
    return `${this.firstName()} ${this.lastName()}`;
  });

  changeName() {
    this.firstName.set('Signal Spider');
    this.lastName.set('Man');
  }
}
Enter fullscreen mode Exit fullscreen mode

The code for the signal version is undeniably cleaner and more straightforward.

Have you noticed that even if you aggressively click the changeName button, the counter never surpasses two?

Hello from Signal Spider Man!

2
Enter fullscreen mode Exit fullscreen mode

We'll discuss this in more depth later, but the short version is that signals only notify dependents when their value actually changes. To replicate this in RxJs, we'd need to add yet another operator—distinctUnitlChanged—to the pipeline.

However, this is not to say that RxJs is obsolete or on its way out. Far from it. RxJs excels at letting developers construct asynchronous, reactive streams with ease. You can listen to an input's change events, debounce its emissions, map them to parameters for an HTTP request, and transform the response into the exact shape needed by the view—all within a single, cohesive stream. This is simply impossible with signals.

It all comes down to this: although both are reactive, RxJs and signals address different problems. They are complementary, not interchangeable. Used together, they will allow for an entire new level of simpler and more robust Angular applications.

For a deeper dive into why RxJs isn't the right reactive primitive for Angular, I highly recommend this piece by Mike Pearson. It was instrumental in helping me fully grasp the limitations of Observable-based inputs.

Signals as the new fundamental

With the problems the Angular team set out to solve now clear, and with an understanding of why RxJs doesn't fit the bill, it's time to meet the protagonist of this piece: signals.

Recall the definition we quickly mentioned from Solid at the outset:

Signals are the cornerstone of reactivity in Solid. They contain values that change over time; when you change a signal's value, it automatically updates anything that uses it.

Before diving into how signals achieve this, it makes sense to examine the API that the Angular team currently ships. Much of this is lifted straight from the README found here. That document is a gem, and reading it once isn't enough — go through it twice or even three times.

Angular signals are functions that take no arguments (() => T). Calling one returns the signal's current value. Invoking a signal has no side effects, although it can lazily recompute intermediate values through lazy memoization.

Certain environments, like template expressions, can be made reactive. In these contexts, reading a signal not only yields its value but also registers it as a dependency of the current context. The context's owner is then alerted whenever one of these dependencies emits a new value, typically causing the expression to be evaluated again so the fresh value is consumed.

Note: This is the source of signals' power for change detection and DOM updates. The template that depends on a signal is notified directly — there's no need to traverse component trees or guess which parts need re-checking.

This pairing of context and getter function means dependencies of a context are picked up automatically and implicitly. There's no need to pre-declare lists of dependencies, and the dependency set doesn't have to stay static between runs.

Note: Contrast this with combineLatest, where you must add every observable to the dependency array before you can access its value later.

Writable signals with signal()

Calling signal() yields what's called a settable signal. Beyond acting as a getter, settable signals offer methods to modify the signal's value and notify any dependent contexts of that change. These include .set to replace the value outright, .update to produce a new value from the current one, and .mutate for in-place changes. These operations are attached to the signal function itself.

const counter = signal(0);

counter.set(2);
counter.update(count => count + 1);
Enter fullscreen mode Exit fullscreen mode

For modifying the signal's existing value in place, there's the dedicated .mutate method:

const todoList = signal<Todo[]>([]);

todoList.mutate(list => {
    list.push({title: 'One more task', completed: false});
});
Enter fullscreen mode Exit fullscreen mode

Note: Signals don't demand immutability to properly inform their dependents of changes!

Equality comparison

When creating a signal, an optional equality comparator function can be supplied. That function determines whether a newly provided value is considered the same as or different from the signal's current value.

If the equality function deems two values identical, the signal will:

  • prevent the value from being updated;
  • refrain from propagating the change.

Derived state with computed()

The computed() function returns a memoized signal whose value is derived from one or more source signals.

const counter = signal(0);

// Automatically updates when `counter` changes:
const isEven = computed(() => counter() % 2 === 0);
Enter fullscreen mode Exit fullscreen mode

Since the computation function runs in a reactive context, any signals it reads are recorded as dependencies. Consequently, the computed signal's value is recalculated whenever any of those dependencies changes.

Just like ordinary signals, a computed signal can accept an optional equality comparator function.

Effects with effect()

effect() arranges for a function that triggers side effects to run in a reactive context. The signal dependencies that the function reads are tracked, and the effect reruns whenever one of those dependencies changes.

const counter = signal(0);
effect(() => console.log('The counter is:', counter()));
// The counter is: 0

counter.set(1);
// The counter is: 1
Enter fullscreen mode Exit fullscreen mode

Effects will not run synchronously in response to a set() call (more on this in the section about glitch-free execution); instead, the framework schedules and handles them. Exactly when an effect fires is left unspecified.

Note: This may sound alarming at first. As developers, behavior that isn't specified tends to worry us. Yet in this case, it grants Angular the freedom to decide when effects should execute, which can be leveraged for performance wins.

Escaping reactivity with untracked()

Note: This one isn't currently in the official README. I'm including it to give a full picture of the API.

This utility keeps the wrapping computation from tracking reads made on the signal passed to it. So even if that signal changes, the surrounding context won't be notified.

const counter0 = signal(0);
const counter1 = signal(0);

// Executes when `counter0` changes, not when `counter1` changes:
effect(() => console.log(counter0(), untracked(counter1));

counter0.set(1);
// logs 1 0
counter1.set(1);
// does not log
counter1.set(2);
// does not log
counter1.set(3);
// does not log
counter0.set(2);
// logs 2 3
Enter fullscreen mode Exit fullscreen mode

That said, whenever the surrounding context runs, it will still read the current value of the untracked signal.

This is where the API stands right now. Once more, I'd urge you to spend time with the README on GitHub. It's truly exceptional and a key resource for anybody working with signals.

Inside the signal engine

The official README gives an excellent breakdown of the internal mechanics, so I will lean on that explanation for the core ideas.

Producers and consumers

At the heart of the signal system are two core interfaces: Producer and Consumer. These are not concrete classes but contracts implemented by the various parts of the reactive runtime.

  • A Producer is anything that can emit change notifications. This includes the different kinds of signals.
  • A Consumer is a reactive context that can read and depend on one or more Producers.

In short, producers are the source of reactivity, and consumers are where that reactivity is observed.

Some entities act as both. A computed, for example, is a consumer of other signals while also producing a new derived value for any downstream observers.

Both Producer and Consumer maintain explicit references to each other through dependency Edges. A Producer knows which Consumers are watching it, and each Consumer tracks the set of Producers it relies on. These associations are always two-way.

This wiring forms a dependency graph that precisely maps the relationships between all reactive nodes.

How change moves through the graph

Earlier, we saw how RxJs Observables can leave us with glitches. Signals have a built-in solution for that, and here’s the mechanism behind it.

The push/pull algorithm

Angular Signals delivers glitch-free behavior by splitting the update process into two distinct stages.

The first stage happens synchronously whenever a Producer changes. The notification of potential change is pushed out to all dependent Consumers, marking their cached values as potentially stale.

Importantly, this stage does not execute side effects or recalculate derived values. It only flags cached results for invalidation.

Once the push phase completes, the second stage begins. Now, when a signal value is requested, any invalidated computed values are recalculated on demand.

This is the "push/pull" model: invalidation is swept out eagerly, but actual recalculation is lazy, happening only when a consumer reads the value it depends on.

Value versioning

This is the most intricate piece of the signal design. We’ll start with the official explanation, then walk through a concrete example.

Every Producer keeps a monotonically increasing valueVersion counter. This counter changes whenever the semantic meaning of the value changes, not just when it’s reassigned. When a Consumer reads a value, it stores the current valueVersion in the dependency Edge.

Before a Consumer performs its reactive work (like running effect code or recomputing a derived value), it checks its dependencies and asks them to refresh their valueVersion if needed. For a computed, this triggers a recalculation and a subsequent equality check. If the value is stale, the polling process recurses, because the computed itself is a Consumer that must check its own Producers first. If the recalculated value differs semantically, the valueVersion increments.

Now the Consumer can compare the new valueVersion against the one stored in its dependency Edge. If all versions match, nothing actually changed, and the consumer can skip its reactive action entirely.

An example will make this concrete.

Suppose we run this code:

const counter = signal(0);
const isEven = computed(() => counter() % 2 === 0);
effect(() => console.log(isEven() ? 'even!' : 'odd!');

counter.set(1);
// logs odd!
counter.set(2);
// this is the change we are going to look at

Push/Pull algorithm for setting signal to 2

Here’s the sequence of events:

  1. Updating the settable signal kicks off the push/pull pipeline.
  2. The Producer counter marks itself dirty and propagates that status to its consumer, isEven.
  3. Since isEven is also a Producer, it forwards the dirty flag to its own consumers—here, the effect. The push phase concludes.
  4. The effect now pulls the current value from isEven.
  5. isEven pulls the current value from counter.
  6. counter reports its new value and updated valueVersion to isEven.
  7. isEven recomputes its output, notices the value changed, and bumps its own valueVersion.
  8. Finally, the effect sees the version change, reads the new value (true), runs its callback, and logs 'even!'.

Now, let’s see what happens when we set the counter to 4.

counter.set(4);

Push/Pull algorithm for setting signal to 4

This time, the steps look a bit different:

  1. A change to the signal value starts the push/pull cycle.
  2. counter propagates the dirty status to isEven.
  3. isEven pushes the notification to the effect. Push phase done.
  4. The effect asks isEven for its current value.
  5. isEven requests the latest value from counter.
  6. counter reports its value as 4 and increments its valueVersion accordingly.
  7. isEven recalculates, confirms its output did not change, and therefore leaves its own valueVersion untouched.
  8. The effect sees that isEven’s version hasn’t changed, so it skips execution entirely.

This combination—eager pushing of invalidation and lazy pulling of values—is what gives signals their glitch-free guarantees.

The same dependency tracking also handles cleanup. When a Producer is no longer needed by any Consumer, it can be garbage collected automatically. This means you never have to manually unsubscribe from a signal. Memory leaks are effectively ruled out by design.

---

Fine-grained reactivity means fine-grained change detection, and that means a performance leap

Because Angular now ships its own reactive primitive, it has a first-class integration path, which lets it take advantage of everything this reactive approach offers for change detection.

One useful way to think of template rendering is as an effect. Something along these lines:

const counter = signal(0);
const doubleCounter = computed(() => counter() * 2);

effect(() => renderTemplate(`
  <div>My counter is: ${counter()}</div>
  <div>My double counter is ${doubleCounter()}</div>
`));
Enter fullscreen mode Exit fullscreen mode

When the template is rendered inside an effect, it turns into a consumer of every signal it reads. Any change to those signals triggers a notification, and we instantly know exactly when to re-render.

To appreciate how much more efficient fine-grained reactivity is at keeping the DOM in sync with the model, it helps to compare the current change detection pipeline with the one signals enable.

Before we look at that, here is what each symbol means in the diagrams below.
Symbols explained

How change detection works in Angular today

Current Angular change detection mechanism

Consider an application made up of multiple components. Some of those components have models that logically depend on one another; others are completely independent. In the DOM tree, all of their views are linked through parent/child relationships.

Angular mirrors this structure with a tree of its own, using a top-down dirty checking algorithm. Even when component data has no real dependency, the framework still forces parent/child relationships between the components.

Then, on every change detection cycle, it makes a single pass down the tree, comparing each binding's old value with the current model value. Every binding gets checked exactly once.

In the diagram, a model inside the component tree has changed, shown as an orange circle. That model has no logical link to any other component. When a new change detection cycle kicks off, this is the sequence:

  • Angular starts at the root of the tree and checks that node.
  • It then keeps walking down the tree, figuring out which components need to be re-evaluated.
  • Finally, it reaches the component where the model changed.
  • The equality check fails, and the DOM is patched.

And this whole sequence repeats for every single change detection trigger.

Now let's see what changes when signals are driving the process.

Signal-powered change detection

Signal based change detection

Take the same application: multiple components, some models with logical dependencies and some without, and all views connected as parent and child nodes in the DOM tree.

With signals, no separate tree is required to enable change detection. The signals that the template reads notify it directly when their values change, so in this abstraction the change detection arrow points straight at the DOM node.

So what happens when the model changes?

The template gets notified, and the DOM is updated.

That's the whole story.

Mind blown

No more walking down a graph from the top.

No more pointless comparisons.

The notification mechanism that tells the framework to refresh the view is now built directly into signals.

Mind blown!

It's hard not to get excited about the performance gains that follow from this level of fine-grained reactivity.

signals + RxJs = <3

At this point, I hope you share my enthusiasm for signals.

I want to close by repeating something important: signals and RxJs are complementary, not competing.

Many applications that already rely on the OnPush strategy, RxJs, and the async pipe are well positioned to benefit immediately from the performance boost that signals bring. The async pipe will eventually be superseded by signals. OnPush will become a thing of the past, since signals can notify Angular whenever a DOM update is required. And RxJs will keep doing what it does best: declaratively modeling complex asynchronous streams.

Together, they will form the foundation of future Angular applications.

Ready. Set. Go.

This is genuinely an exciting moment for the Angular community. I am 1000% convinced that signals will dramatically improve both the developer experience and the user experience of Angular applications. They present a single, simple model for updating views. They are performant, reactive, and destined to become a core part of Angular.

I hope you now have everything you need to take full advantage of signals when they arrive later this year.

As always, do you have questions or ideas for future posts? Are you as excited about signals as I am, or do you see issues the team still needs to solve? I'd love to hear your thoughts — feel free to leave a comment or reach out directly.

And if you enjoyed this article, please like and share it. To keep up with my content, you can follow me on Twitter or Github.