When people start with Angular, they often ask:
- Why
{{ }}sometimes, and[innerHTML]other times? - Why
signalin one place, andObservablein another? - Why
computedandeffectboth 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 valueeffect= 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(optionallytoSignalfor template) - Plain text in template =>
{{ }} - HTML content in template =>
[innerHTML](carefully)
