When people start with Angular, they often ask:

  • Why {{ }} sometimes, and [innerHTML] other times?
  • Why signal in one place, and Observable in another?
  • Why computed and effect both exist?

This guide explains it in plain language, with practical examples and the “why” behind each choice.

1) {{ }} vs [innerHTML]

Think of this as:

  • {{ }} = show text safely
  • [innerHTML] = render real HTML markup

{{ }} (Interpolation): safe text output

Use when your content is plain text (name, title, number, translation string without HTML rendering needs).

<!-- Good for plain text -->
<p>{{ user.name }}</p>
<p>{{ 'my.translation.key' | translate }}</p>
Why?

Angular escapes HTML automatically for security.

If the value contains <strong>S5</strong>, Angular shows it as text, not bold formatting.
That is intentional protection against XSS (malicious script injection).

[innerHTML]: render HTML tags

Use only when your data actually contains HTML tags you want to display as formatted content.

<!-- Good for HTML content -->
<p [innerHTML]="'my.translation.key' | translate"></p>
<p [innerHTML]="descriptionWithHtml"></p>
Why?

Because here you are telling Angular: “interpret this string as HTML”.

So <strong>S5</strong> becomes bold S5 in the UI.

Security warning (important)

Never inject uncontrolled user content directly into [innerHTML].

<!-- Dangerous if userInput is untrusted -->
<div [innerHTML]="userInput"></div>

Angular sanitizes by default, but you should still be careful with source trust and review your data path.

Rare advanced case:

import { DomSanitizer } from '@angular/platform-browser';
const safe = this.sanitizer.bypassSecurityTrustHtml(myHtml);

Use this bypass only if you fully trust and control the HTML source.


2) signal vs Observable

Think of this as:

  • signal = local component state (simple, synchronous)
  • Observable = external or asynchronous stream (HTTP, websocket, events over time)

signal: local reactive state

readonly count = signal(0);
readonly nodes = signal<RenderNode[]>([]);

this.count.set(5);
this.count.update(v => v + 1);

console.log(this.count());

Why?
  • Very easy to read/write
  • No manual subscribe/unsubscribe
  • Great for UI state like selected item, counters, local lists

Observable: async flow over time

this.http.get('/api/data').subscribe(data => { ... });

fromEvent(document, 'click').pipe(
  debounceTime(300)
).subscribe(ev => { ... });
Why?

Some data arrives later or repeatedly (server responses, live events, timers).
That is exactly what Observables are built for.

PS: Must manage subscription lifecycle to avoid memory leaks.

private readonly destroyRef = inject(DestroyRef);

this.myService.data$.pipe(
  takeUntilDestroyed(this.destroyRef)
).subscribe(data => {
  this.nodes.set(data);
});

Bridge between both

readonly data = toSignal(this.myService.data$, { initialValue: [] }); // Observable -> Signal
readonly data$ = toObservable(this.mySignal);                         // Signal -> Observable

Use this when you need async data in template-friendly signal form.


3) computed vs effect

Think of this as:

  • computed = calculate a value
  • effect = do something when value changes

computed: pure derived value

readonly nodes = signal<RenderNode[]>([]);

readonly switchNodes = computed(() =>
  this.nodes().filter(n => n.kind === 'switch')
);

readonly trackCount = computed(() =>
  this.nodes().filter(n => n.kind === 'track').length
);
Why?
  • Automatic recalculation only when dependencies change
  • Cached/memoized
  • No side effects
  • Ideal for values displayed in UI

effect: side effects

constructor() {
  effect(() => {
    const model = this.model();
    this.nodes.set(model?.nodes ?? []);
    console.log('Model changed:', model);
  });
}
Why?

Use it for actions, not values:

  • logging
  • syncing state
  • calling services
  • DOM interactions

Common mistake

Using effect to compute state that should be computed:

// Bad pattern
effect(() => {
  this.switchNodes.set(this.nodes().filter(n => n.kind === 'switch'));
});

// Better
readonly switchNodes = computed(() =>
  this.nodes().filter(n => n.kind === 'switch')
);

Final decision map (simple)

  • Local UI state => signal
  • Value derived from signal => computed
  • React and perform action => effect
  • HTTP / websocket / async streams => Observable (optionally toSignal for template)
  • Plain text in template => {{ }}
  • HTML content in template => [innerHTML] (carefully)

By Shabazz

Software Engineer, MCSD, Web developer & Angular specialist , BizDevOps

Leave a Reply

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