Angular Signals: A New Mental Model for Reactivity, Not Just a New API

Angular Signals are not just another feature. They represent a different way to think about data flow. If you’re coming from RxJS or standard @Input() / @Output()
bindings, you might think of Signals as a simpler syntax for observable values. However, that’s like saying a violin is just a smaller cello. The shape of the instrument affects the kind of music you create.
In this article, I won’t repeat the documentation or guide you through another counter example. Instead, I’ll introduce a new way of thinking that Signals enable, along with some real challenges I faced when using them in production.
Signals as Reactive Variables, Not Streams
Think of Signals as reactive variables, not data streams. This is the key change in perspective. In RxJS, we typically push values down a stream, combine them, and respond with side effects through subscribe(). Signals turn this concept around. You read from them like variables. Angular automatically tracks dependencies and triggers reactions.
Here’s the best way I explain Signals in code:
const firstName = signal('John');
const lastName = signal('Doe');
const fullName = computed(() => `${firstName()} ${lastName()}`);
In this example, fullName is automatically recalculated when either firstName or lastName changes. You don’t need to think in terms of map, combineLatest, or teardown logic. You just declare relationships.
If this feels like Vue or SolidJS, it’s not a coincidence.
Gotcha #1: Implicit Dependencies Can Backfire
When you read from a Signal inside a computed() or effect(), Angular tracks that read as a dependency. But this can go wrong fast when you’re not aware of those reads.
let counter = signal(0);
const doubled = computed(() => {
console.log('Recomputing...');
return counter() * 2;
});
You might expect this to run only when the counter changes, but if you accidentally read another Signal inside the same function (e.g. a logging flag), it becomes a dependency too. Suddenly, toggling a debug mode flag starts recalculating your math logic.
Tip: Keep computed and effect logic narrow and deterministic. Otherwise, you’ll have phantom updates you can’t debug.
Signals vs. RxJS: Where Signals Shine – and Where They Don’t
Let’s be clear: Signals don’t replace RxJS. They’re designed to work alongside it. But understanding when to use each is critical.
Use Case | Prefer Signals | Prefer RxJS |
Local component state | ✓ | X |
Derived UI data | ✓ | X |
Event streams (e.g. user typing) | X | ✓ |
Shared state across modules (via service Signals) | ✓ | ✓ |
Complex async flows with retries | X | ✓ |
Signals excel at modelling value over time. RxJS excels at modelling events over time.
Gotcha #2: Computed Signals Don’t Cache Like You Think
A surprising thing I discovered: computed() doesn’t memoize like React’s useMemo() or even like you might expect from a getter.
Each time you read from a computed() signal, the logic re-runs if its inputs have changed. But if you’re calling it multiple times in a template (say in *ngIf and again in {{ }}), you may pay the cost more than once.
Tip: If a computation is expensive, consider storing it in a local const in the component class and just referencing that in the template. Or wrap it in another signal.
Rethinking State Shape: Signals Love Flat, Not Deep
In classic Angular with services and RxJS, it’s common to model state like this:
const state$ = new BehaviorSubject({
user: null,
settings: {},
isLoading: false
});
In Signals, deeply nested reactive objects are awkward. You can’t say user().settings().theme() – that’s a read on a read on a read. Instead, you’ll want to flatten:
const user = signal<User | null>(null);
const settings = signal<Settings>({});
Tip: Model each piece of state with its own signal. You’ll gain flexibility and easier reactivity control.
Practical Scenario: Form Label Customization
Let’s say you have a SearchSidebarComponent, and you want to customize its labels from a parent. Here’s the naive way:
@Input() labels = signal<MapSidebarLabels>();
What happens if you try to derive a computed value?
labelsFinal = computed(() => {
const raw = this.labels();
return { ...raw, title: raw.title.toUpperCase() };
});
Now, suppose in the parent you write:
<search-sidebar [labels]="labelsFinal()" />
This works, but you’re calling a signal within a signal. And if you change your template to <search-sidebar [labels]="labelsFinal" />
, it fails with a type mismatch.
Tip: Angular’s input system isn’t yet fully signal-native. Until it is, flatten the value before passing inputs.
Gotcha #3: effect() Runs Immediately – And Maybe Again
Unlike RxJS subscribe(), which fires only when something emits, effect() fires once on creation, even if the signal hasn’t changed yet.
effect(() => {
console.log("API call for", userId());
});
This will run once immediately, even if userId hasn’t changed yet. Be careful when placing side effects like HTTP calls or analytics tracking inside effect().
Tip: Guard effect() logic with null checks or early returns if needed.
Final Thoughts
Signals in Angular aren’t just a new syntax, they’re a shift in mental model. Once you stop thinking in observables and start thinking in variables that react, you’ll find your components are smaller, faster, and easier to reason about.
But like any new tool, Signals come with sharp edges. Know the tradeoffs, learn the patterns, and above all, don’t treat Signals like fancy getters. They’re much more powerful than that, but only if you understand the model.
Sonu Kapoor is a Google Developer Expert (GDE) in Angular, a Microsoft MVP, and a long-time contributor to the frontend ecosystem. With 20+ years in web development - from the early days of IE6 and ActiveX to today’s modern frameworks - he has authored two technical books and contributed to the Angular framework itself. He regularly writes for developer platforms and speaks at global tech conferences.