Sitemap
Piyali Das 🧡🤍💚

Piyali Das 🧡🤍💚

Senior Frontend Engineer | Angular Expert | AI Enthusiast https://www.linkedin.com/in/piyalidas10/

Angular 19 SSE using EventSource

5 min readJan 28, 2026

--

Server-Sent Events (SSE) is a web technology that enables a server to automatically push real-time text-based updates to a client (such as a web browser) over a single, persistent HTTP connection. The client uses the EventSource JavaScript interface to receive these updates.

In zoneless Angular, async APIs like EventSource don’t trigger change detection. We use signals to store streamed data and manually call ApplicationRef.tick() — ideally batched — to update the UI. This gives us predictable performance and avoids zone-related overhead.

Press enter or click to view image in full size

✅ Zoneless Angular SSE Service

✔ No NgZone
✔ No recursive ApplicationRef.tick()
✔ Batched updates
✔ Signal-based
✔ Production-ready

“SSE events don’t trigger Angular change detection, so signal updates alone don’t re-render. We manually schedule change detection.”

import {
Injectable,
signal,
ApplicationRef,
OnDestroy,
inject,
} from '@angular/core';

export interface SseMessage {
id?: string;
data: any;
}

@Injectable({ providedIn: 'root' })
export class SseService implements OnDestroy {
private appRef = inject(ApplicationRef);
private eventSource?: EventSource;

// ===== SIGNAL STATE =====
readonly messages = signal<SseMessage[]>([]);
readonly status = signal<'idle' | 'connected' | 'error' | 'disconnected'>(
'idle'
);

// ===== INTERNAL BUFFER =====
private buffer: SseMessage[] = [];
private flushTimer?: number;

// ===== TICK CONTROL (CRITICAL) =====
private tickScheduled = false;

// ===== CONNECT =====
/**
EventSource
↓
buffer.push()
↓ (every 100ms)
signal.update()
↓
queueMicrotask()
↓
ApplicationRef.tick() ← ONCE

In zoneless Angular, SSE runs completely outside Angular. We batch events into signals and schedule ApplicationRef.tick() asynchronously to avoid recursive change detection.
*/
connect(url: string, options?: { withCredentials?: boolean }) {
this.disconnect();

this.eventSource = new EventSource(url, {
withCredentials: options?.withCredentials ?? true,
});

this.eventSource.onopen = () => {
this.status.set('connected');
this.scheduleTick();
};

this.eventSource.onmessage = (event) => {
// ❗ NO Angular calls here
this.buffer.push({
id: event.lastEventId || undefined,
data: this.safeParse(event.data),
});
};

this.eventSource.onerror = () => {
this.status.set('error');
this.scheduleTick();
};

this.startFlushLoop();
}

// ===== FLUSH BUFFER (BATCHED) =====
private startFlushLoop() {
this.flushTimer = window.setInterval(() => {
if (!this.buffer.length) return;

const batch = [...this.buffer];
this.buffer.length = 0;

this.messages.update((m) => [...m, ...batch]);
this.scheduleTick(); // ✅ SAFE
}, 100);
}

// ===== SAFE MANUAL CHANGE DETECTION =====
private scheduleTick() {
if (this.tickScheduled) return;

this.tickScheduled = true;

queueMicrotask(() => {
this.tickScheduled = false;
this.appRef.tick(); // ✅ NEVER recursive
});
}

// ===== DISCONNECT =====
disconnect() {
this.eventSource?.close();
this.eventSource = undefined;

if (this.flushTimer) {
clearInterval(this.flushTimer);
this.flushTimer = undefined;
}

this.status.set('disconnected');
this.scheduleTick();
}

ngOnDestroy() {
this.disconnect();
}

private safeParse(data: string) {
try {
return JSON.parse(data);
} catch {
return data;
}
}
}

👇Why Signals alone are NOT sufficient ? ApplicationRef.tick() will be required ?

yes — in zoneless Angular, signals alone are NOT sufficient.
ApplicationRef.tick() (or something equivalent) is required.

1️⃣ Why signals feel like they should be enough (but aren’t)

Signals do trigger change detection…
⚠️ only when Angular knows “something happened.”

In zone-based Angular:

Browser async event
↓ (zone.js patches it)
Angular is notified
↓
Signals update → UI updates automatically

So you never think about it.

2️⃣ What changes in zoneless Angular

When you remove Zone.js:

EventSource / setTimeout / WebSocket
↓
Angular is NOT notified
↓
Signals update silently
❌ UI does NOT re-render

Signals update state, but Angular never runs change detection.

Get Piyali Das 🧡🤍💚’s stories in your inbox

Join Medium for free to get updates from this writer.

👉 Signals are reactive, not magical.

3️⃣ The rule (this is the key insight)

Signals update data
Change detection updates the DOM

In zoneless Angular:

  • Angular will not run CD on its own
  • You must explicitly tell Angular to render

4️⃣ When ApplicationRef.tick() is required

You need tick() when ALL of these are true:

| Condition        | Value                         |
| ---------------- | ----------------------------- |
| Zoneless Angular | ✅ |
| Async source | SSE / WebSocket / setInterval |
| Outside Angular | Browser API |
| Updating signals | ✅ |

Only when change detection runs again:

ApplicationRef.tick()

During that tick:

  • Dirty views are checked
  • Templates re-run
  • Signals are re-read
  • DOM updates

➡️ tick is mandatory

5️⃣ Why not ChangeDetectorRef.detectChanges()?

❌ detectChanges()

  • Requires component reference
  • Breaks easily with lazy / routed / destroyed views
  • Hard to scale across services

✅ ApplicationRef.tick()

  • Global
  • Safe for services
  • Works across routes
  • Official zoneless pattern

Angular core team explicitly recommends it for zoneless apps.

6️⃣ Why NOT call tick() directly every time?

Because this happens 💥:

tick()
↳ CD runs
↳ signal updates
↳ tick()
↳ NG0101 💀

That’s why you must schedule it asynchronously:

queueMicrotask(() => appRef.tick());

This avoids recursive change detection.

7️⃣ Is there ANY way to avoid tick()?

✅ Yes — but only in very specific cases

| Case                     | tick needed? |
| ------------------------ | ------------ |
| Click / input event | ❌ |
| Router navigation | ❌ |
| HttpClient | ❌ |
| EventSource / WebSocket | ✅ |
| setInterval / setTimeout | ✅ |

Why?
Because Angular already runs CD for framework-managed events.

Zone-based Angular:
async → CD → signals → DOM

Zoneless Angular:
async → signals → ❌ DOM
↑
tick() needed

Now main question is “Why needs Change Detection using appRef.tick() when we have used Signals ??? “

✅ Your statement (100% correct)

“Here no Angular event is firing, that’s why change detection is not running. So we have to manually trigger CD.”

✔️ Yes. That is precisely what’s happening.

Signals notify Angular only if Angular is already running change detection.

They do NOT start change detection on their own. Signals are reactive state, not a scheduler.

✅ Why this happens (step-by-step)

✔️ EventSource fires

  • Browser-native API
  • Angular does not know it happened

✔️ Signal value changes

  • State is updated
  • Angular is not currently rendering

✔️ Template is not re-evaluated

  • Signals are not re-read
  • DOM stays the same

✔️ Solution

  • Manually tell Angular:
    👉 “Hey, re-render now”

That’s what ApplicationRef.tick() does.

“Signals update state, not the DOM.
In zoneless Angular, we must explicitly trigger rendering for async sources like SSE.”

Angular event (no manual CD)

<button (click)="add()">Add</button>
add() {
this.messages.update(m => [...m, 'new']);
}

Why it works:

  • (click) is an Angular-managed event
  • CD already runs
  • Signal is read → DOM updates

Browser event (manual CD required)

eventSource.onmessage = () => {
this.messages.update(m => [...m, 'new']);
};

Why it doesn’t:

  • Angular never scheduled CD
  • Signal updated in isolation
  • DOM never re-renders

Mental model (lock this in)

Angular renders ONLY when it decides to render
Signals don’t decide that
Events do

SSE ≠ Angular event.

Why Angular does NOT auto-detect signal writes

If Angular re-rendered on every signal write:

  • Infinite loops would be easy
  • Performance would collapse
  • Zoneless would be pointless

So Angular keeps:

  • State change (signals)
  • Render scheduling (CD)

separate.

When Zoneless + SSE is a GREAT idea

| Scenario               | Recommendation         |
| ---------------------- | ---------------------- |
| AI streaming | ✅ Excellent |
| Dashboards | ✅ Excellent |
| High-frequency updates | ✅ With batching |
| Forms / CRUD apps | ❌ Overkill |
| Small apps | ❌ Not worth complexity |

--

--