skip to content

In Angular, what does calling ApplicationRef.tick() do compared with ChangeDetectorRef.detectChanges(), and why is calling it from application code rarely right?

level: middleimportance: should knowfreq 35%

answer

  1. whole application versus one subtree
  2. root views, effects, render hooks
  3. dev-mode second pass
  4. calling it from inside a tick
  5. zoneless changes what it refreshes

basics

~20 s

ApplicationRef.tick() synchronously runs change detection for every view attached to the application, plus root effects and render hooks, with a dev-mode verification pass; detectChanges() checks one subtree. Schedulers already call tick(), so manual calls usually duplicate work or fail as recursive ticks.

solid answer

~40 s

`ApplicationRef.tick()` is the application-wide pass: it processes all root views attached to the `ApplicationRef`, flushes root effects, runs `afterNextRender`/`afterEveryRender` hooks and, in development mode, follows with a check that nothing changed during the pass. In a zone-based app it checks globally, meaning `Eager` views and dirty `OnPush` views; in a zoneless app it refreshes only views that were marked or have dirty signal consumers. `ChangeDetectorRef.detectChanges()` checks just one view and its descendants. Angular's schedulers already call `tick()` at the right moment, and a manual call cancels any tick they had scheduled. Calling it while a tick is running throws `NG0101`. So manual calls in app code usually mean state is changing somewhere Angular cannot see; the fix is to make it notify through signals or `markForCheck()`.

go deeper

for a junior

Know that ApplicationRef.tick() runs change detection for the whole application, while detectChanges() checks a single component subtree.

for a middle

Describe what a tick includes (root effects, views, render hooks, dev-mode verification) and why calling it recursively throws NG0101.

for a senior

Explain the zone-based global versus zoneless targeted behaviour, recognise manual tick() calls as a symptom of untracked state, and know the few integration cases that justify them.

for a principal

Keep manual application-wide checks out of feature code by policy, confining them to integration layers with clear ownership and tests.

## Two scopes of manual checking Angular exposes manual checking at two levels: | API | Scope | Typical caller | |---|---|---| | `ChangeDetectorRef.detectChanges()` | one view and its descendants | a component managing its own view | | `ApplicationRef.tick()` | every root view attached to the application | Angular's own schedulers | `ApplicationRef` is the application-level service, available with `inject(ApplicationRef)`. Its root views are the bootstrapped component plus any view attached with `attachView()`, for example a dynamically created component rendered outside the normal tree. ## What tick() does A call to `tick()` runs synchronously and: 1. flushes **root effects** that are dirty, 2. refreshes the attached **views**, 3. repeats while views are still dirty (bounded; in development mode, endless re-dirtying fails with `NG0103`), 4. runs **render hooks** registered with `afterNextRender()` and `afterEveryRender()`, 5. in **development mode**, runs a verification pass over the views to detect bindings that changed during the pass. Which views step 2 refreshes depends on the scheduling mode: - **Zone-based** (`provideZoneChangeDetection()`): a global check, so `Eager` views and dirty `OnPush` views are refreshed. This is what the zone scheduler calls after `onMicrotaskEmpty`. - **Zoneless** (default since v21): a targeted check, so only views that were marked for refresh or have a dirty signal consumer are refreshed. Calling `tick()` does not force unmarked `Eager` views to update. ## Why manual calls are rarely right ### The scheduler already does it In zone-based apps the zone calls `tick()` after each task; in zoneless apps the scheduler calls it after notifications such as signal writes, `markForCheck()`, template events and input changes. A manual call either repeats work that was about to happen or, according to the scheduler's own logic, **cancels** the tick it had scheduled and runs one now, changing timing in ways the rest of the app does not expect. ### Recursion throws Calling `tick()` while a tick is running, for example from a lifecycle hook, from a template-bound method, or from code triggered synchronously during rendering, throws **`NG0101`** ("ApplicationRef.tick is called recursively"). ### It usually hides the real problem If the screen only updates after a manual `tick()`, some state changed without notifying Angular: a plain field written in a third-party callback, a mutation of an object an `OnPush` input already holds, or work in a zoneless app that used to rely on zone.js. Forcing an application-wide pass fixes the symptom at the cost of checking everything. ## Legitimate uses - **Integration code** that renders Angular views outside the normal tree, such as a component attached with `ApplicationRef.attachView()` inside a third-party overlay, and needs a pass after creating it. - **Low-level tooling** and libraries that drive Angular from a non-Angular host. - **Tests** use `ComponentFixture` methods instead (`fixture.detectChanges()`, `await fixture.whenStable()`), not `tick()`. Even in these cases, marking the view (`markForCheck()`) is often enough in zoneless apps, because the notification schedules a tick. ## detectChanges() versus tick(), summarised - `detectChanges()`: one subtree, synchronous, ignores the attached flag of the view it is called on, leaves ancestors alone. - `tick()`: every attached root view, synchronous, runs effects and render hooks, adds a dev-mode verification pass, and is the unit of work both schedulers use. ## Quick recall - `tick()` = whole application; `detectChanges()` = one subtree. - `tick()` runs root effects, views and render hooks, then a dev-mode verification pass. - Zone-based: global check; zoneless: targeted check of marked or signal-dirty views. - Re-entrant `tick()` throws `NG0101`; endless re-dirtying in dev mode throws `NG0103`. - A manual `tick()` cancels the tick the scheduler had already planned. ## Related removal `ChangeDetectorRef.checkNoChanges()`, the public way to run only the verification pass, was removed in v22. The pass still runs as part of `tick()` in development mode; tests trigger it through `fixture.detectChanges()`.

  • In a zoneless Angular app, why might calling ApplicationRef.tick() not update an Eager component whose field you just changed?
    Zoneless ticks run in targeted mode: they refresh views marked for refresh or with dirty signal consumers. A plain field assignment marks nothing, so the Eager view is not refreshed even though a tick ran. Calling `markForCheck()` on that component, or using a signal, gives the tick something to refresh.
  • What does NG0103 mean after a tick?
    In development mode, Angular re-ran the refresh loop the maximum number of times and views were still dirty, typically because something marks a view for check during every template execution or an after-render hook keeps marking views. It points to a feedback loop between rendering and state updates.

saying these in an interview costs you the question

  • ApplicationRef.tick() only checks the component that calls it
  • Calling tick() inside ngAfterViewInit is a safe way to flush changes
  • In a zoneless app, tick() re-checks every Eager view regardless of marking
  • tick() skips effects and render hooks; it only updates bindings
  • Tests should call ApplicationRef.tick() instead of fixture methods