Published on

Angular Learning Summary 1 - Signals

Authors
  • avatar
    Name
    Jinjiu Liu
    Twitter

Content

I started learning fastAPI and Angular for my Portfolio Tracking project. It's been a long time since my previous blog, was busy handling lots of things. I hope this year will be a fruitful one with lot of good changes! Fingers crossed!

Today it's a simple blog about the signals and forms in Angular. They allow us to build reactive variables in the html and bind with reactive values. However, there are a few different concepts and use cases.

Signals in Angular

1. signal() (The Standard Writable Signal)

The foundational building block. It holds a value and notifies the template exactly when that value changes.

  • Concept: A box containing data that you can read and write to locally.
  • How to define it (TS):
import { signal } from '@angular/core';

// Type is inferred as number
count = signal(0); 

  • How to use it (HTML): Call it as a function to read the value.
<p>Current count: {{ count() }}</p>

  • How the value is updated:
  • .set(value): Completely overwrites the current value.
  • .update(fn): Uses the current value to calculate the new one.
this.count.set(5); 
this.count.update(current => current + 1);

  • Best Practice & Use Case: Use for local component state (e.g., toggles, counters, loading spinners). Keep them private or protected so outside components cannot randomly manipulate your state.

2. computed() (The Derived Signal)

A read-only signal that automatically calculates its value based on one or more other signals.

  • Concept: The "Math Equation." It automatically recalculates when its dependencies change, and it perfectly caches the result so it doesn't waste CPU cycles.
  • How to define it (TS):
import { signal, computed } from '@angular/core';

price = signal(10);
quantity = signal(5);

// Automatically becomes 50
total = computed(() => this.price() * this.quantity());

  • How to use it (HTML):
<p>Total Value: ${{ total() }}</p>

  • How the value is updated: It updates automatically. You cannot use .set() or .update() on a computed. It reacts instantly whenever price or quantity change.
  • Best Practice & Use Case: Use for filtering, math, and derived data. Crucial Rule: Never execute side effects inside a computed function (like saving to a database or updating another signal). It must be pure logic.

3. linkedSignal() (The Resettable State Signal)

A writable signal that grabs its initial value from a different signal (like an input()), but automatically resets if the parent pushes new data.

  • Concept: "Draft Mode." Let the user edit a local copy, but throw away the edits if the server pushes fresh data.
  • How to define it (TS):
import { input, linkedSignal } from '@angular/core';

serverQuantity = input.required<number>();

// Starts as the server value, but can be overwritten locally
draftQuantity = linkedSignal(() => this.serverQuantity());

  • How to use it (HTML): Usually bound to an input field.
<input type="number" [(ngModel)]="draftQuantity">

  • How the value is updated: The user updates it locally via typing, or you use .set(). However, if the parent component sends a new serverQuantity, the draftQuantity instantly forgets the user's edits and resets to match the new server data.
  • Best Practice & Use Case: Use for Edit Forms. This completely eliminates the need for lifecycle hooks like ngOnInit or ngOnChanges when you need to populate a form with database data.

4. input() (The Signal Input)

The modern replacement for the @Input() decorator. It allows a parent to pass data down, acting as a read-only signal inside the child.

  • Concept: A one-way data pipe from parent to child.
  • How to define it (TS):
import { input } from '@angular/core';

// Optional input with a default value
theme = input<'dark' | 'light'>('light'); 

// Required input (Compiler throws an error if parent forgets it)
holding = input.required<HoldingInfo>(); 

  • How to use it (HTML):
<h2>Holding Name: {{ holding().name }}</h2>

  • How the value is updated: It is updated by the parent component's HTML. The child component cannot modify it locally (it is strictly read-only).
<app-child [holding]="myHoldingData"></app-child>

  • Best Practice & Use Case: Use for passing data down the component tree. Always prefer input.required() if your component will break without that specific data.

5. model() (The Two-Way Binding Signal)

The modern replacement for the @Input() and @Output() pair. It allows data to flow down from a parent, be edited by the child, and automatically sync back up to the parent.

  • Concept: A two-way communication channel between parent and child.
  • How to define it (TS):
import { model } from '@angular/core';

// Child component declares the model
isVisible = model<boolean>(false);

  • How to use it (HTML): The parent uses "banana-in-a-box" syntax.
<app-modal [(isVisible)]="parentVisibilitySignal"></app-modal>

  • How the value is updated: Inside the child component, you treat it like a normal writable signal using .set() or .update(). The moment you update it, the parent's signal is instantly updated too.
  • Best Practice & Use Case: Use for building custom UI controls (e.g., custom checkboxes, sliders, dropdowns, or modals). Avoid overusing this for standard data; stick to input() unless the child must mutate the parent's state.