skip to content

A parent writes <app-volume-slider (valueChange)="persist()" [(value)]="volume" /> and persist() always saves the previous volume in Angular; why, and how do you fix it?

level: seniorimportance: should knowfreq 36%

answer

  1. two listeners on one output
  2. subscribed in template order
  3. the write happens in the second
  4. use $event, not the field

basics

~10 s

Both bindings subscribe to the same valueChange output in template order, so persist() runs before the two-way listener writes the new value. Pass $event to persist, or at least move the handler after [(value)].

solid answer

~40 s

`[(value)]` expands to an event binding on `valueChange`, so this element has two listeners on the same output. Angular's compiler emits listeners in the order the attributes appear, and an output calls its subscribers in subscription order. The explicit `(valueChange)="persist()"` comes first, so when the slider emits, `persist()` runs and reads `this.volume()` while it still holds the old value; only then does the two-way listener call `volume.set($event)`. The robust fix is to stop reading the field in the handler and use the emitted value: `(valueChange)="persist($event)"`. Reordering the attributes also works but is fragile, because a formatter or a colleague can swap them back. Moving persistence into an `effect()` that reads `volume` is another option when every change should be saved.

code

ts · 18 lines
ts
import { Component, signal } from '@angular/core';
import { VolumeSlider } from './volume-slider';

@Component({
  selector: 'app-player',
  imports: [VolumeSlider],
  template: `
    <!-- persist receives the emitted value, so listener order no longer matters -->
    <app-volume-slider [(value)]="volume" (valueChange)="persist($event)" />
  `,
})
export class Player {
  volume = signal(40);

  persist(level: number) {
    localStorage.setItem('volume', String(level));
  }
}

go deeper

for a junior

Recall that [(value)] creates its own valueChange listener next to any you add.

for a middle

Explain that listeners on one output run in template order and synchronously on emit.

for a senior

Diagnose the one-step-behind bug and fix it with $event or a single explicit handler rather than attribute order.

for a principal

Turn it into a team rule: side-effect handlers beside two-way bindings read $event, and lint or review for [(x)] plus (xChange).

## What the template really contains `[(value)]="volume"` is shorthand for a property binding plus an event binding on `valueChange`. The element in the question therefore has **two listeners on the same output**: ```text listener 1 (valueChange)="persist()" written by you property [value]="volume()" from [(value)] listener 2 (valueChange)="volume.set($event)" from [(value)] ``` Nothing forbids this; adding a side effect next to a two-way binding is a common pattern. The problem is the order in which the two listeners run. ## Why the order is the template order 1. Angular's template compiler keeps event listeners, including the listener generated for a two-way binding, **in the order the attributes appear** on the element. The compiled creation code registers listener 1, then listener 2. 2. Each registration subscribes to the child's `valueChange` output. 3. When the child emits (with `valueChange.emit(n)` or, for a `model()`, `value.set(n)`), the output calls its subscribers **in subscription order**, synchronously. So on every emission: | Step | What runs | `this.volume()` at that moment | |---|---|---| | 1 | `persist()` | still the old value | | 2 | two-way listener: `volume.set($event)` | the new value | | 3 | next change detection pass pushes the new value down | the new value | `persist()` reads the field in step 1, so it always saves the value from **before** the drag. The bug looks random in testing because the saved value is only one step behind, and a user who pauses between drags rarely notices. ## Fixes, from most to least robust - **Use the emitted value.** `(valueChange)="persist($event)"` with `persist(level: number)`. The handler no longer depends on whether the two-way write has happened yet, so attribute order stops mattering. - **Derive the side effect from state.** If every change to `volume` must be saved, an `effect()` that reads `volume()` and calls the save function runs after the write, whoever made it, including the slider, a keyboard shortcut or a reset button. Effects run asynchronously as part of change detection, so this suits persistence but not logic that must run in the same tick as the event. - **Split the two-way binding.** `[value]="volume()" (valueChange)="onVolume($event)"`, where `onVolume` sets the signal and then persists. One listener, explicit order, and room for validation. - **Reorder the attributes.** Putting `(valueChange)` after `[(value)]` makes `persist()` see the new value, but the correctness now depends on attribute order that formatters, reviewers and future edits do not know about. Treat it as a quick patch, not a fix. ## Why Angular does not reorder them for you It would be convenient if the listener generated by `[( )]` always ran first, but the compiler deliberately treats it as an ordinary listener and keeps source order. That behaviour is predictable and documented by the generated code: in the compiled output, the two-way listener and a regular listener appear exactly where their attributes were written. A special priority rule would also be surprising in the other direction, for handlers that intentionally want to see the old value, such as an undo stack that records the previous level before it changes. The practical conclusion is not to rely on either order: a handler that needs the new value gets it from `$event`, and a handler that needs the old value should read it explicitly and say so in its name. ## The same trap elsewhere The ordering is not specific to custom components. `<input [(ngModel)]="name" (ngModelChange)="search()">` has the same shape. With `(ngModelChange)` written first, `search()` reads the stale `name`. The long-standing advice for `ngModel` is the same: use `$event` in the extra handler. ## Diagnosing it in a real app - Log inside the handler both `$event` and the field. If they differ by one step, it is ordering. - Search templates for an element that has both `[(x)]` and `(xChange)`; each one is a candidate. - A unit test that emits from the child and asserts on the saved value catches it immediately, because emission and both listeners run synchronously. The general rule to state in the interview: **a handler next to a two-way binding should take its data from `$event`, never from the field the two-way binding is about to write.**

  • Why is an effect() a reasonable place to persist a two-way bound signal in Angular?
    An effect that reads `volume()` runs after the signal has been written, no matter which code wrote it, so every source of change is saved once. The trade-off is timing: effects run asynchronously during change detection, not inside the event, so they suit persistence but not work that must happen in the same tick.
  • Does the Angular listener order problem affect [(ngModel)] with an extra (ngModelChange)?
    Yes. `[(ngModel)]` expands to an `(ngModelChange)` listener, and two listeners on the same output run in template order. A `(ngModelChange)` written before `[(ngModel)]` reads the old field value; passing `$event` avoids the issue.

saying these in an interview costs you the question

  • The two-way write always happens before any other listener runs
  • Angular runs listeners on the same output in random order
  • Reordering the attributes is a robust long-term fix
  • An output emits asynchronously, so the field is already updated
  • The bug only happens with model(), not with plain input+output pairs