In Angular, what is the difference between ChangeDetectorRef.markForCheck() and ChangeDetectorRef.detectChanges()?
answer
- now versus next pass
- up the tree or down the tree
- who runs the check
- which one is cheap to repeat
basics
~10 smarkForCheck() only marks the view and its ancestors dirty and notifies the scheduler, so the next change-detection pass refreshes them; detectChanges() synchronously checks this view and its descendants right now, without touching ancestors.
solid answer
~40 s`markForCheck()` walks **up** from the component's view to the root, flagging each view as dirty, and notifies Angular's change-detection scheduler. Nothing is checked at that moment; the next pass, a zone-triggered tick or a scheduled zoneless tick, refreshes those views even if they are `OnPush`. It is cheap and safe to call several times. `detectChanges()` runs change detection **down** from this view **synchronously**: this view is refreshed and its children are processed by their own strategy. It does not update parents or siblings, and it works on a detached view, which is why it pairs with `detach()`. Use `markForCheck()` when an `OnPush` view's state changed somewhere Angular does not track, such as a manual subscription; reserve `detectChanges()` for when you need the DOM updated before the current code continues, or for a detached subtree.
go deeper
Remember the one-line difference: markForCheck() schedules a refresh of the view and its ancestors, detectChanges() checks this view and its children right now.
Explain why markForCheck() marks ancestors, how clean OnPush children are skipped by detectChanges(), and how zoneless mode turns markForCheck() into a scheduled tick.
Justify each call in review: prefer signals or the async pipe, allow markForCheck() for untracked callbacks, and restrict detectChanges() to detached views or synchronous DOM reads.
Treat ChangeDetectorRef usage as a code-health metric and steer teams toward state that marks views itself, so manual calls become rare, documented exceptions.
## The two calls at a glance `ChangeDetectorRef` is the handle Angular gives a component (or directive) to control change detection for its own view. You get it with `inject(ChangeDetectorRef)`. Two of its methods are asked about more than any other: | | `markForCheck()` | `detectChanges()` | |---|---|---| | When work happens | at the next change-detection pass | immediately, synchronously | | Direction | up: this view and every ancestor | down: this view and its descendants | | Runs a check itself | no, it only flags and notifies | yes | | Affects ancestors | marks them dirty | no | | Works on a detached view | the view stays skipped until reattached | yes, that is its main use | | Cost of calling repeatedly | tiny: flags already set stay set | a full subtree check each call | ## markForCheck(): "please look at me next time" `markForCheck()` sets a dirty flag on the component's view and on each ancestor up to the root view, then notifies Angular's scheduler. - In a **zone-based** app, the next tick (after the current zone task) refreshes the flagged views. - In a **zoneless** app (the default since v21), the notification itself schedules a tick. Why ancestors? Change detection runs from the root down. An `OnPush` parent that is not dirty is skipped along with its children, so marking only the child would not be enough. Marking the path to the root guarantees the pass reaches the component. Typical use: an `OnPush` component (the default since v22) whose field is set inside a manual `subscribe()` or a third-party callback. ```ts import { ChangeDetectorRef, Component, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { QuoteFeed } from './quote-feed'; @Component({ selector: 'app-last-price', template: `<span>{{ price }}</span>`, }) export class LastPrice { private readonly cdr = inject(ChangeDetectorRef); protected price = 0; constructor() { inject(QuoteFeed).price$('ACME') .pipe(takeUntilDestroyed()) .subscribe(p => { this.price = p; this.cdr.markForCheck(); }); } } ``` ## detectChanges(): "check me now" `detectChanges()` flags the view for refresh and immediately runs change detection starting from it: 1. The view's template bindings are evaluated and the DOM is updated. 2. Its children are visited: `Eager` children and dirty `OnPush` children are refreshed; clean `OnPush` children are skipped. 3. Control returns to your code with the DOM already updated. It does **not** walk up. If a parent's template shows the same state, the parent stays stale until something else checks it. ## Choosing between them 1. **Default to neither.** Signals read in the template, the `async` pipe, template event listeners and input changes already mark views. Needing `ChangeDetectorRef` at all is a sign that state lives outside those channels. 2. **If you must, prefer `markForCheck()`.** It cooperates with the scheduler, batches with other changes, and updates ancestors consistently. 3. **Use `detectChanges()` deliberately:** for a detached view you refresh on your own schedule, or when code must read the updated DOM synchronously right after changing state. ## Common mistakes in interviews and code - Saying `markForCheck()` "runs change detection": it only flags views and notifies the scheduler. - Expecting `detectChanges()` to update a parent that shows the same data: it only goes down. - Calling `detectChanges()` in a loop or on every stream emission: each call is a full synchronous subtree check, with no batching. - Calling `markForCheck()` on a view that already reads the value through a signal or the `async` pipe: harmless, but redundant. - Assuming `markForCheck()` helps a detached view: detached views are skipped even when dirty. - Forgetting that zoneless apps need a notification at all: a plain field write marks nothing, while `markForCheck()` both marks and schedules. ## A note on checkNoChanges() `ChangeDetectorRef` used to have a third checking method, `checkNoChanges()`. It was deprecated in v17 and **removed in v22**; application code was never meant to call it, and tests use `fixture.detectChanges()` instead. ## The signal alternative In current Angular, the `LastPrice` example is usually written with a signal: `price = toSignal(feed.price$('ACME'), { initialValue: 0 })`, read as `price()` in the template. The signal read marks the view when the value changes, so neither method is needed.
- Why does markForCheck() mark every ancestor and not just the component's own view?Change detection starts at the root and skips a clean `OnPush` view together with its whole subtree. If only the child were flagged, the pass could stop at a clean `OnPush` ancestor and never reach it. Marking the path to the root guarantees the pass walks down to the component.
- A parent and its child both display a value that the child updates and then calls detectChanges(). What does the user see?The child's DOM shows the new value immediately, but the parent's binding stays stale, because `detectChanges()` only checks downward from the child. The parent updates only when something else checks it. `markForCheck()`, or a shared signal read by both templates, keeps them consistent.
saying these in an interview costs you the question
- markForCheck() runs change detection immediately
- detectChanges() also refreshes the component's parents
- markForCheck() is expensive, so call it only once per component
- detectChanges() is the preferred way to update an OnPush component
- Current Angular apps should call checkNoChanges() after detectChanges()