skip to content

In Angular, how would you use ChangeDetectorRef.detach() and detectChanges() to refresh a live stock-ticker widget once per second, and what does detaching cost you?

level: seniorimportance: should knowfreq 40%

answer

  1. leave the tree, refresh on your clock
  2. skipped even when dirty
  3. inputs and signals stop rendering
  4. reattach() to rejoin
  5. throttle the data instead

basics

~20 s

Call detach() so ticks skip the widget, then call detectChanges() from a one-second interval to render the latest quotes; while detached, nothing else refreshes it, not input changes, markForCheck() or signal reads, until reattach() or the next manual check.

solid answer

~50 s

A price feed can push dozens of quotes a second, far more than anyone can read. Calling `detach()` removes the widget's view from the change-detection tree: ticks skip it **even if it is marked dirty**. The component keeps storing the latest quotes in a field, and a `setInterval` calls `detectChanges()` every second, which synchronously checks the view and its children. The costs are that the widget no longer reacts to anything on its own: new inputs from a parent, `markForCheck()` and signal changes read in its template are ignored until the next manual check or `reattach()`. You must also clear the interval on destroy. In current Angular I would often keep the view attached and throttle the **data** instead, for example `auditTime(1000)` feeding a signal, because the component stays declarative; `detach()` is justified when the subtree is expensive and you need full control over when it renders.

go deeper

for a junior

Know that detach() takes a component out of automatic change detection and that detectChanges() refreshes it manually.

for a middle

Explain that detached views are skipped even when dirty, and list what stops rendering: inputs, markForCheck(), signal reads, template events.

for a senior

Build the throttled widget with cleanup, weigh it against throttling the data with auditTime and a signal, and note the zone-based tick the interval still causes.

for a principal

Decide whether imperative render control belongs in a shared dashboard library at all, and how to document and test components that opt out of the tree.

## The scenario A dashboard shows a stock-ticker widget. The quote feed emits many updates per second per symbol. Rendering each one wastes CPU and makes numbers flicker faster than anyone can read them. The goal: render at most once per second, whatever the feed does. ## Detach and refresh on your own clock ```ts import { ChangeDetectorRef, Component, DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Quote, QuoteFeed } from './quote-feed'; @Component({ selector: 'app-stock-ticker', template: ` @for (q of quotes; track q.symbol) { <span class="quote">{{ q.symbol }} {{ q.price }}</span> } `, }) export class StockTicker { protected quotes: Quote[] = []; constructor() { const cdr = inject(ChangeDetectorRef); cdr.detach(); inject(QuoteFeed).quotes$ .pipe(takeUntilDestroyed()) .subscribe(q => (this.quotes = q)); const id = setInterval(() => cdr.detectChanges(), 1000); inject(DestroyRef).onDestroy(() => clearInterval(id)); } } ``` What each piece does: 1. **`detach()`** clears the view's "attached" flag. During every change-detection pass Angular skips detached views and everything below them. The API docs are explicit: detached views are not checked **even if they are marked as dirty**. 2. The subscription only stores the latest quotes. No rendering happens on emission. 3. **`detectChanges()`** every second checks this view and its descendants synchronously, regardless of the attached flag. 4. **`DestroyRef.onDestroy`** clears the interval so no checks run on a destroyed component. This is the same pattern the `ChangeDetectorRef` API documentation uses for a large, constantly changing list refreshed every few seconds. ## What detaching costs | Change source | Attached view | Detached view | |---|---|---| | New input from the parent | marks and refreshes the view | value is set, screen unchanged | | `markForCheck()` | refreshed on the next pass | still skipped | | Signal read in the template changes | refreshed on the next pass | still skipped | | Event listener in the template fires | refreshed | skipped until a manual check | | `detectChanges()` | refreshed immediately | refreshed immediately | So the widget becomes a small island that renders only when you say so. That is the point, and also the risk: a parent passing a new `symbols` input, or a user clicking a "sort" button inside the widget, sees no effect until the next interval fires. ## reattach() `reattach()` puts the view back into the tree, so normal passes refresh it again. The API docs pair it with a `live` toggle: an `effect()` reading a `live` input calls `reattach()` when true and `detach()` when false. That is useful for "pause live updates" controls. ## A zone.js side effect In a zone-based app, the one-second `setInterval` itself runs in the Angular zone, so every second it also triggers an application-wide tick, even though the widget is skipped. Starting the interval outside the Angular zone avoids that; it is the zone-pollution fix. In zoneless apps (the default since v21) the timer triggers nothing on its own, and `detectChanges()` needs no scheduler. ## The declarative alternative Often the better tool is to throttle the **data**, not the **view**: - pipe the feed through `auditTime(1000)` or `sampleTime(1000)`, - expose it as a signal with `toSignal()`, or through the `async` pipe, - keep the component attached and `OnPush` (the default since v22). The widget then renders once a second because its data changes once a second, and inputs, events and signals keep working normally. ## Checklist before shipping a detached widget - The interval (or other refresh trigger) is cleared on destroy. - Every input that should show immediately either triggers `detectChanges()` itself or is documented as refreshed on the next interval. - Template events inside the widget call `detectChanges()` if their result must appear at once. - There is a test that advances time and asserts the refreshed DOM. ## When detach() is still justified - The subtree is expensive (hundreds of cells) and you need exact control over render timing. - Several data sources feed it and throttling each is awkward. - You want "freeze" behaviour, for example while the user hovers to read a value. Document it in the component, because the next developer will otherwise be puzzled why an input change does not show.

  • Does a signal read in a detached component's template still update the screen?
    No. The signal change marks the view's reactive consumer as dirty, but change-detection passes skip detached views entirely. The new value appears only when the component calls `detectChanges()` or `reattach()` and a pass runs.
  • Why clear the interval in DestroyRef.onDestroy rather than relying on the view being gone?
    The interval belongs to the browser, not to Angular, so it keeps firing after the component is destroyed and keeps the component instance reachable. Clearing it on destroy stops needless calls and lets the component be garbage-collected.

Detaching is like taking a live scoreboard off the stadium's automatic feed and having an operator press 'update' once a second. The board stays calm, but it also ignores the referee's corrections until the operator presses the button.

saying these in an interview costs you the question

  • A detached view still refreshes when it is marked with markForCheck()
  • detectChanges() does nothing on a detached view until reattach() is called
  • Signals bypass detach(), so a detached template still updates
  • detach() stops the component's subscriptions and timers
  • Throttling with detach() is always better than throttling the data stream