How would you implement two-way binding for a custom component (Input/Output naming convention), and what can go wrong?

MediumIntermediateAngular
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

Custom two-way binding uses an @Input() value and a paired @Output() valueChange event. The binding syntax [(value)] listens to valueChange and updates value. Mismatched names or missing emitters break the binding. Covers: angular, two way binding, input, output, components, forms.

Answer

Core idea

[(x)] works only if Angular can find:
@Input() x
@Output() xChange

Then this:
<app-cmp [(x)]="state"></app-cmp><br>is equivalent to:
<app-cmp [x]="state" (xChange)="state = $event"></app-cmp>

TYPESCRIPT
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-text-input',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <label>
      <span class="sr-only">Name</span>
      <input
        [value]="value"
        (input)="onInput(($event.target as HTMLInputElement).value)"
      />
    </label>
  `
})
export class TextInputComponent {
  @Input() value = '';
  @Output() valueChange = new EventEmitter<string>();

  onInput(next: string): void {
    // Do NOT rely on mutating @Input() to update parent.
    // Always emit the change.
    this.valueChange.emit(next);
  }
}
                  
HTML
<!-- parent.component.html -->
<app-text-input [(value)]="name"></app-text-input>

<p>Preview: {{ name }}</p>
                  

Aliasing still works (but names must still match)

If you alias the input name, the output alias must be that alias + Change.

TYPESCRIPT
import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-toggle',
  template: `
    <button type="button" (click)="toggle()">
      {{ checked ? 'ON' : 'OFF' }}
    </button>
  `
})
export class ToggleComponent {
  @Input('checked') checked = false;
  @Output('checkedChange') checkedChange = new EventEmitter<boolean>();

  toggle(): void {
    this.checkedChange.emit(!this.checked);
  }
}

// usage:
// <app-toggle [(checked)]="isEnabled"></app-toggle>
                  

What can go wrong

Symptom

Fix

Naming mismatch (e.g., @Output() valueChanged instead of valueChange)

[(value)] fails to compile / template error

Use exact convention: input <prop> + output <prop>Change (or matching aliases)

You mutate the @Input() and forget to emit

Child UI updates, parent state does not

Treat @Input as read-only; emit through the Output for parent updates

Emitting at the wrong time (during init hooks)

ExpressionChangedAfterItHasBeenCheckedError / flicker

Emit from user actions or schedule (e.g., queueMicrotask/setTimeout) if you must emit after init

Two sources of truth (internal state diverges from input)

UI shows stale value or “snaps back”

Render from the @Input; if you keep local state, sync it carefully in ngOnChanges

Using [(...)] with a non-assignable expression

Template error: can't assign to expression

Bind to a writable field: [(value)]="name" (not "getName()" / pipes / literals)

Objects/arrays are mutated instead of replaced (especially with OnPush patterns)

Hard-to-debug stale UI / shared state side effects

Emit new references (immutable updates) or clearly own mutation boundaries

Using custom two-way binding for form controls when you really need Angular Forms integration

ngModel / reactive forms don’t work properly (touched/dirty/validation)

Implement ControlValueAccessor for true form controls

Common pitfalls with custom two-way binding

Practical notes

Watch for edge case behavior, common pitfalls, and trade-offs between clarity and performance. Mention accessibility and testing considerations when the concept affects UI output or event timing.

Summary

Custom two-way binding is just [prop] + (propChange). Implement @Input() prop and @Output() propChange, emit on user interaction, avoid mutating inputs as your update mechanism, and use ControlValueAccessor when the component is a real form control.

Similar questions
Guides
8 / 37