Signals & JS Event Loop: Rethinking Angular Reactive Sync

signals-&-js-event-loop:-rethinking-angular-reactive-sync

🟩 Signal definition

Signals are synchronous reactive primitives containers, that hold an immutable single value

🟩 Signals & Immutability

Signals are immutable, but the value they hold is not

Signals in Angular are reactive primitives. Once you create a signal using signal(initialValue), that signal instance doesn’t change. You don’t replace the signal with another one; you update its internal state using the methods set() or update().

🟩🟩 Immutability with signals means:
🔸 The reference to the signal doesn’t change.
🔸 It encapsulates its reactive behavior and dependencies.
🔸 Reactivity is predictable and traceable because the structure doesn’t change over time

If the value of the Signal is a reference object (eg. an object or array) you can still modify that reference without calling the set() or update(). In practice, this means the absolute value of the Signal can change without alerting all the consumers of the Signal.

🟩 Signal mutate immutability

🚨 Signals are immutable containers, but if the value they hold is a reference (like an object or array), that reference can be mutated directly, which bypasses Angular reactivity system.

const user = signal({ name: 'Alice', age: 30 });

user().name = 'Bob';  // BAD 

user.set({ ...user(), name: 'Bob' }); // GOOD
const users = signal<User[]>([
  { id: 1, name: 'Alice', age: 30 },
  { id: 2, name: 'Bob', age: 25 },
  { id: 3, name: 'Carol', age: 35 },
])

// Update a user’s name immutably
users.set(users().map(user =>
  user.id === 2 ? { ...user, name: 'Robert' } : user
))

//  Add a new user immutably
users.set([
  ...users(),
  { id: 4, name: 'Dave', age: 28 }
]);

// 3. Remove a user immutably
users.set(users().filter(user => user.id !== 1))

🟩 Angular Change-Detection & Signals

Angular is transitioning to fine-grained reactivity. Because with default change detection, there is no way for Angular to know exactly what has changed on the page, so that is why we cannot make any “assumptions” about what happened, and we need to check everything.

Signals are all about enabling very fine-grained updates to the DOM that are just not possible with the current change detection systems that we have available.

Simply put, a signal is a reactive primitive that represents a value and that allows us to granular change that same value in a controlled way and track its changes over time.

🟩 JS Event-Loop and Signal

Event-loop is critical for Angular reactivity and performance

🟩🟩 Angular Change Detection & Zone.js

  • Zone.js Hooks:
    Zone.js patches both micro-and macrotask APIs so that every async callback (Observable emission included) triggers change detection.
  • Microtask Checkpoint:
    Angular waits for the microtask queue to drain before running change detection, ensuring all Promise and Observable-driven state changes are applied consistently.
event loop  microtask drained  Zone.js  Angular runs change detection cycle

🟩🟩 Where Signals Change the Story

Signals break the blanket dependency on event loop + Zone.js

Signals step in by breaking the blanket dependency on the event loop + Zone.js, Instead of waiting for any microtask to finish, a signal knows exactly which pieces of state are “watched” by which consumers.

When a signal changes, Angular doesn’t need to run global change detection. It updates only the components (or computed values) that directly depend on that signal.

This avoids unnecessary trips through the whole component tree every time the event loop drains.

console.log("Start");

setTimeout(() => {
  console.log("Timeout callback fired");
  stateSignal.set("timeout update");
}, 0);

Promise.resolve().then(() => {
  console.log("Promise callback fired");
  stateSignal.set("promise update");
});

console.log("End");

🔸 Without signals:

Start
End
Promise callback fired
 Change Detection (whole app checked)
Timeout callback fired
 Change Detection (whole app checked)

🔸 with signals:

Start
End
Promise callback fired
 Signal updated, only dependent component updated
Timeout callback fired
 Signal updated, only dependent component updated

Signals let Angular ride on top of the event loop without being bound to Zone.js heavy blanket strategy. They reduce unnecessary CD cycles by updating only what is truly reactive.

🟩 Angular has 4 “kind” of signals and Angular Change Detection cycle

🔸 WritableSignal = read & write state. Trigger CD when: set, update, mutate. created by: signal()
🔸 Signal = read-only state. No trigger CD (doesn’t mark the component as dirty) created by: .asReadonly()
🔸 ComputeSignal = Pure functions of other signals. No side effects (Never modify state (e.g., no set(), API calls, or DOM updates)). Signal that get from other signal. No trigger CD (not directly). created by: computed()
🔸 LinkedSignal() (experimental) = Hybrid: Starts as a computed signal (pure, memoized, lazy), but allows dynamic switching of its source signal. It’s read-only and reactive, but the source can be writable.

// WritableSignal
const ws = signal(0);  // WritableSignal

const currentValue = ws(); // ✅ READ OPERATION (NO CD TRIGGER)

// WRITE OPERATIONS (TRIGGER CD)
ws.set(5);            // 🚨 Replaces value → TRIGGER CD
ws.update(v => v + 1); // 🚨 Derives new value → TRIGGER CD
ws.mutate(obj => obj.prop = 1); // 🚨 In-place mutation → TRIGGER CD (if ws holds an object)
// Signal
const ros = ws.asReadonly();  // Signal (read-only)

// READ OPERATION (NO CD TRIGGER)
const currentValue = ros(); // ✅ Reads value → No CD

// ATTEMPTED WRITES (BLOCKED)
ros.set(5);      // 🚨 Compile-time error (TypeScript protects you)
ros.update(...); // 🚨 Compile-time error
// ComputedSignal 
const double = computed(() => ws() * 2); // ComputedSignal

// ✅ Read (no CD trigger, but reacts to ws() changes)
console.log(double()); // 10 (if ws() is 5)

// 🚨 Write Attempts (Blocked)
double.set(20); // ❌ TS Error: No 'set' method
// LinkedSignal
const base = signal(2);
const linked = linkedSignal({
  source: () => base() * 2,
  computation: (val) => val + 1,
});

linked.set(10); // can override

🟩 Angular Signal Types & Change Detection Behavior

This table summarizes the four core types of Angular Signals and how they interact with change detection.

Signal Type Writable Derived Triggers CD Created By
WritableSignal signal()
Readonly Signal .asReadonly()
ComputedSignal ❌ (indirect) computed()
LinkedSignal linkedSignal()

🟩 Take away notes

  • WritableSignal: Mutable state container. Triggers change detection on .set(), .update(), or .mutate().
  • Readonly Signal: Exposes state without allowing mutation. Does not trigger change detection.
  • ComputedSignal: Derived from other signals using pure functions. Angular tracks reads during render; does not directly trigger CD.
  • LinkedSignal: Hybrid signal — behaves like a computed signal but can also be written to. Triggers change detection on write.

Use this table to guide architectural decisions when designing reactive state in Angular with Signals.

🟩 When to use which

  • signal() – WritableSignal
    ❌ Pure, Can have side effects. You need to modify the value (set, update, mutate)
    ❌ Lazy, updates eagerly.
    ❌ Memorized
    ✅ Trigger CD directly, on write (set, update, mutate)

🔸 Think of it as: a plain reactive variable. Typical use: component-local state that changes often: flags

const count = signal(0);
count.set(1);
  • signal.asReadonly() – asReadOnly()
    ✅ Pure, read-only
    ✅ Lazy
    ✅ Memorized
    ❌ Triggers CD directly, but ✅ indirectly when the underlying writable changes.

🔸Typically for Local component or service state (eg. form inputs, API responses, expose state from service)

const writable = signal(42);
const readonly = writable.asReadonly();
  • computed() – pure derived from other signal
    ✅ Pure, read-only derived from signals.
    ✅ Lazy, Angular only recalculates when you read it
    ✅ Memorized
    ❌ Triggers CD directly, but ✅ indirectly when the underlying writable changes.
    🔸 Typical use: derived, reactive values that are always deterministic. Formatting, filtering, or combining signals.
    🔸 Great when your value is purely reactive
fullName = computed(() => firstName() + ' ' + lastName())
  • LinkedSignal() – read-only reactive mirror signal
    ✅ Pure (read-only mirror)
    ✅ Lazy
    ✅ Memorized
    ❌ Triggers CD directly, but ✅ indirectly when the underlying writable changes.
    🔸It takes a source signal and returns a new signal that mirrors it.
    🔸 Writable behaviour comes only from the sources, not the linkedSignal itself.
    🔸 Use anywhere you need a signal that can change its dependency at runtime, like theming
linkedSignal({ source, computation }).

💯 Thanks!

Hey, don’t be an stranger. Let’s stay in touch!


leolanese’s GitHub image

🔘 Portfolio: www.leolanese.com

🔘 Linkedin: LeoLanese

🔘 Twitter: @LeoLanese

🔘 DEV.to: Tech Blog

🔘 Questions / Suggestion / Recommendation: developer@leolanese.com

🟩 Observables & Signals: A real analogy

If you’re like me, coming from RxJS and reactive programming in Angular, this analogy helped things click quicker:

Think of Observables like a water pipe: once you connect (subscribe), you start getting the flow. Signals are more like a glass of water, always present, always filled with the latest value. When the value changes, it’s like someone replaced the water, and everything watching it gets notified instantly.

In UI terms, Observables stream events, Signals reflect state. Making it visible, so we can react granularly to what is, not just what is emitted.

🟩 More about Signals:
🔸 Angular 20 BehaviorSubjects to Signals

🔸 Angular 20 Modern Forms Strategies and trends

🔸 Angular 20 Forms Performance Fetch Search and Filtering

🔸 Angular 20 API Observable and Signal

🔸 Angular 19 Component Communication

🔸 Angular 19 Zoneless signals

🔸 Angular19 API Signal

🔸 Angular 19 Signals Theme

🔸 Angular 17 Signals Shopping Cart

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
examining-capabilities-driven-ai

Examining Capabilities-Driven AI

Next Post
onto-innovation’s-4d-technology-approved-as-fanuc-cobot-device-supplier-with-launch-of-new-integration-plug-in

Onto Innovation’s 4D Technology Approved as FANUC Cobot Device Supplier with Launch of New Integration Plug-In

Related Posts