Interview answer drill

Use this Angular interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.

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

HighIntermediateAngular
Interview focus

This Angular interview question tests whether you can explain you create two-way binding in a custom Angular component, connect it to production trade-offs, and handle common follow-up questions.

  • you create two-way binding in a custom Angular component explanation without falling back to memorized docs wording
  • Two Way Binding and Input reasoning, edge cases, and production failure modes
  • How you would answer the most likely Angular interview follow-up
Practice more Angular interview questions
Interview 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.

Full interview 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
TYPESCRIPT
// Broken alias pair: Angular expands [(checked)] to [checked] + (checkedChange)
@Input('checked') checked = false;
@Output('checkedChanged') checkedChanged = new EventEmitter<boolean>(); // wrong name

// Fixed alias pair
@Output('checkedChange') checkedChange = new EventEmitter<boolean>();

// Shadow-state pattern: sync the local draft from the input
@Input() value = '';
draft = '';

ngOnChanges(): void {
  this.draft = this.value;
}

commit(): void {
  this.valueChange.emit(this.draft);
}
                  

Boundary to mention in senior answers

Custom two-way binding is enough when the parent just needs [value] plus (valueChange). If you keep a local draft, sync it from the input and emit the next value instead of letting two sources of truth drift apart. It stops being enough when the component must behave like a real Angular form control with disabled state, touched/dirty tracking, validation, and formControlName/ngModel integration. That is the point where ControlValueAccessor becomes the right abstraction.

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
Preparing for interviews?

Use this as one explanation rep, then continue with the Angular interview questions cluster or a guided prep path.