{"id":4288,"date":"2026-04-17T18:51:45","date_gmt":"2026-04-17T16:51:45","guid":{"rendered":"https:\/\/nguenkam.com\/blog\/?p=4288"},"modified":"2026-04-17T18:52:49","modified_gmt":"2026-04-17T16:52:49","slug":"angular-decision-guide-vs-innerhtml-signal-vs-observable-computed-vs-effect","status":"publish","type":"post","link":"https:\/\/nguenkam.com\/blog\/index.php\/2026\/04\/17\/angular-decision-guide-vs-innerhtml-signal-vs-observable-computed-vs-effect\/","title":{"rendered":"Angular Decision Guide: {{ }} vs [innerHTML], signal vs Observable, computed vs effect"},"content":{"rendered":"\n<p>When people start with Angular, they often ask:<\/p>\n\n\n\n<ul><li>Why&nbsp;<code>{{ }}<\/code>&nbsp;sometimes, and&nbsp;<code>[innerHTML]<\/code>&nbsp;other times?<\/li><li>Why&nbsp;<code>signal<\/code>&nbsp;in one place, and&nbsp;<code>Observable<\/code>&nbsp;in another?<\/li><li>Why&nbsp;<code>computed<\/code>&nbsp;and&nbsp;<code>effect<\/code>&nbsp;both exist?<\/li><\/ul>\n\n\n\n<p>This guide explains it in <strong>plain language<\/strong>, with practical examples and the \u201cwhy\u201d behind each choice.<\/p>\n\n\n\n<h4>1)&nbsp;<code>{{ }}<\/code>&nbsp;vs&nbsp;<code>[innerHTML]<\/code><\/h4>\n\n\n\n<p>Think of this as:<\/p>\n\n\n\n<ul><li><code>{{ }}<\/code>&nbsp;=&nbsp;<strong>show text safely<\/strong><\/li><li><code>[innerHTML]<\/code>&nbsp;=&nbsp;<strong>render real HTML markup<\/strong><\/li><\/ul>\n\n\n\n<h4><span class=\"has-inline-color has-vivid-cyan-blue-color\"><code>{{ }}<\/code>&nbsp;(Interpolation): safe text output<\/span><\/h4>\n\n\n\n<p>Use when your content is plain text (name, title, number, translation string without HTML rendering needs).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- Good for plain text --&gt;\n&lt;p&gt;{{ user.name }}&lt;\/p&gt;\n&lt;p&gt;{{ 'my.translation.key' | translate }}&lt;\/p&gt;\n<\/code><\/pre>\n\n\n\n<h5>Why?<\/h5>\n\n\n\n<p>Angular escapes HTML automatically for security.<\/p>\n\n\n\n<p>If the value contains <code>&lt;strong&gt;S5&lt;\/strong&gt;<\/code>, Angular shows it as text, not bold formatting.<br>That is intentional protection against XSS (malicious script injection).<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h4><span class=\"has-inline-color has-vivid-cyan-blue-color\"><code>[innerHTML]<\/code>: render HTML tags<\/span><\/h4>\n\n\n\n<p>Use only when your data actually contains HTML tags you want to display as formatted content.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- Good for HTML content --&gt;\n&lt;p &#91;innerHTML]=\"'my.translation.key' | translate\"&gt;&lt;\/p&gt;\n&lt;p &#91;innerHTML]=\"descriptionWithHtml\"&gt;&lt;\/p&gt;\n<\/code><\/pre>\n\n\n\n<h5>Why?<\/h5>\n\n\n\n<p>Because here you are telling Angular: \u201cinterpret this string as HTML\u201d.<\/p>\n\n\n\n<p>So <code>&lt;strong&gt;S5&lt;\/strong&gt;<\/code> becomes <strong>bold S5<\/strong> in the UI.<\/p>\n\n\n\n<h5><span class=\"has-inline-color has-luminous-vivid-orange-color\">Security warning (important)<\/span><\/h5>\n\n\n\n<p><em>Never inject uncontrolled user content directly into <code>[innerHTML]<\/code>.<\/em><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!-- Dangerous if userInput is untrusted --&gt;\n&lt;div &#91;innerHTML]=\"userInput\"&gt;&lt;\/div&gt;\n<\/code><\/pre>\n\n\n\n<p><em>Angular sanitizes by default, but you should still be careful with source trust and review your data path.<\/em><\/p>\n\n\n\n<p>Rare advanced case:<code> <\/code><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { DomSanitizer } from '@angular\/platform-browser';\nconst safe = this.sanitizer.bypassSecurityTrustHtml(myHtml);\n<\/code><\/pre>\n\n\n\n<p><strong><em><span class=\"has-inline-color has-luminous-vivid-orange-color\">Use this bypass only if you fully trust and control the HTML source.<\/span><\/em><\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>2)&nbsp;<code>signal<\/code>&nbsp;vs&nbsp;<code>Observable<\/code><\/h4>\n\n\n\n<p>Think of this as:<\/p>\n\n\n\n<ul><li><code>signal<\/code>&nbsp;=&nbsp;<strong>local component state (simple, synchronous)<\/strong><\/li><li><code>Observable<\/code>&nbsp;=&nbsp;<strong>external or asynchronous stream (HTTP, websocket, events over time)<\/strong><\/li><\/ul>\n\n\n\n<h4><span class=\"has-inline-color has-vivid-cyan-blue-color\"><code>signal<\/code>: local reactive state<\/span><\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>readonly count = signal(0);\nreadonly nodes = signal&lt;RenderNode&#91;]&gt;(&#91;]);\n\nthis.count.set(5);\nthis.count.update(v =&gt; v + 1);\n\nconsole.log(this.count());\n\n<\/code><\/pre>\n\n\n\n<h5>Why?<\/h5>\n\n\n\n<ul><li>Very easy to read\/write<\/li><li>No manual subscribe\/unsubscribe<\/li><li>Great for UI state like selected item, counters, local lists<\/li><\/ul>\n\n\n\n<h4><span class=\"has-inline-color has-vivid-cyan-blue-color\"><code>Observable<\/code>: async flow over time<\/span><\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>this.http.get('\/api\/data').subscribe(data =&gt; { ... });\n\nfromEvent(document, 'click').pipe(\n  debounceTime(300)\n).subscribe(ev =&gt; { ... });\n<\/code><\/pre>\n\n\n\n<h5>Why?<\/h5>\n\n\n\n<p>Some data arrives later or repeatedly (server responses, live events, timers).<br>That is exactly what Observables are built for.<\/p>\n\n\n\n<p><strong>PS:<\/strong><em><span class=\"has-inline-color has-luminous-vivid-orange-color\"><strong> Must manage subscription lifecycle to avoid memory leaks.<\/strong><\/span><\/em><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>private readonly destroyRef = inject(DestroyRef);\n\nthis.myService.data$.pipe(\n  takeUntilDestroyed(this.destroyRef)\n).subscribe(data =&gt; {\n  this.nodes.set(data);\n});\n<\/code><\/pre>\n\n\n\n<h4><span class=\"has-inline-color has-vivid-cyan-blue-color\">Bridge between both<\/span><\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>readonly data = toSignal(this.myService.data$, { initialValue: &#91;] }); \/\/ Observable -&gt; Signal\nreadonly data$ = toObservable(this.mySignal);                         \/\/ Signal -&gt; Observable\n<\/code><\/pre>\n\n\n\n<p><em><span class=\"has-inline-color has-luminous-vivid-orange-color\"><strong>Use this when you need async data in template-friendly signal form.<\/strong><\/span><\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>3)&nbsp;<code>computed<\/code>&nbsp;vs&nbsp;<code>effect<\/code><\/h4>\n\n\n\n<p>Think of this as:<\/p>\n\n\n\n<ul><li><code>computed<\/code>&nbsp;=&nbsp;<strong>calculate a value<\/strong><\/li><li><code>effect<\/code>&nbsp;=&nbsp;<strong>do something when value changes<\/strong><\/li><\/ul>\n\n\n\n<h4><span class=\"has-inline-color has-vivid-cyan-blue-color\"><code>computed<\/code>: pure derived value<\/span><\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>readonly nodes = signal&lt;RenderNode&#91;]&gt;(&#91;]);\n\nreadonly switchNodes = computed(() =&gt;\n  this.nodes().filter(n =&gt; n.kind === 'switch')\n);\n\nreadonly trackCount = computed(() =&gt;\n  this.nodes().filter(n =&gt; n.kind === 'track').length\n);\n<\/code><\/pre>\n\n\n\n<h5>Why?<\/h5>\n\n\n\n<ul><li>Automatic recalculation only when dependencies change<\/li><li>Cached\/memoized<\/li><li>No side effects<\/li><li>Ideal for values displayed in UI<\/li><\/ul>\n\n\n\n<h4><span class=\"has-inline-color has-vivid-cyan-blue-color\"><code>effect<\/code>: side effects<\/span><\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>constructor() {\n  effect(() =&gt; {\n    const model = this.model();\n    this.nodes.set(model?.nodes ?? &#91;]);\n    console.log('Model changed:', model);\n  });\n}\n<\/code><\/pre>\n\n\n\n<h5>Why?<\/h5>\n\n\n\n<p>Use it for actions, not values:<\/p>\n\n\n\n<ul><li>logging<\/li><li>syncing state<\/li><li>calling services<\/li><li>DOM interactions<\/li><\/ul>\n\n\n\n<h4><span class=\"has-inline-color has-vivid-cyan-blue-color\">Common mistake<\/span><\/h4>\n\n\n\n<p>Using <code>effect<\/code> to compute state that should be <code>computed<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Bad pattern\neffect(() =&gt; {\n  this.switchNodes.set(this.nodes().filter(n =&gt; n.kind === 'switch'));\n});\n\n\/\/ Better\nreadonly switchNodes = computed(() =&gt;\n  this.nodes().filter(n =&gt; n.kind === 'switch')\n);\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>Final decision map (simple)<\/h4>\n\n\n\n<ul><li>Local UI state =>\u00a0<code>signal<\/code><\/li><li>Value derived from signal =>\u00a0<code>computed<\/code><\/li><li>React and perform action =>\u00a0<code>effect<\/code><\/li><li>HTTP \/ websocket \/ async streams =>\u00a0<code>Observable<\/code>\u00a0(optionally\u00a0<code>toSignal<\/code>\u00a0for template)<\/li><li>Plain text in template =>\u00a0<code>{{ }}<\/code><\/li><li>HTML content in template =>\u00a0<code>[innerHTML]<\/code>\u00a0(carefully)<\/li><\/ul>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>When people start with Angular, they often ask: Why&nbsp;{{ }}&nbsp;sometimes, and&nbsp;[innerHTML]&nbsp;other times? Why&nbsp;signal&nbsp;in one place, and&nbsp;Observable&nbsp;in another? Why&nbsp;computed&nbsp;and&nbsp;effect&nbsp;both exist? This guide explains it in plain language, with practical examples and the \u201cwhy\u201d behind each choice. 1)&nbsp;{{ }}&nbsp;vs&nbsp;[innerHTML] Think of this as: {{ }}&nbsp;=&nbsp;show text safely [innerHTML]&nbsp;=&nbsp;render real HTML markup {{ }}&nbsp;(Interpolation): safe text output Use [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1965,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[37],"tags":[920,1138,736,1075,992,382,927,803,1136,735,1137],"_links":{"self":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/4288"}],"collection":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=4288"}],"version-history":[{"count":2,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/4288\/revisions"}],"predecessor-version":[{"id":4290,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/4288\/revisions\/4290"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/media\/1965"}],"wp:attachment":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=4288"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=4288"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=4288"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}