Skip to the solution
MediumFrontend11 min read

Signal inputs input() vs @Input() — what changes in Angular 17.1+?

637 views

The problem

Question presented to candidate: "Our codebase is full of @Input() fields with ngOnChanges blocks that recompute things. Since Angular 17.1 there is input(). What actually changes if we switch, and is there anything we lose?"

What a strong answer should cover:

  • input() returns a read-only signal (InputSignal<T>); the template and class read it by calling it, label().
  • Derived values become a computed() instead of ngOnChanges or a setter, and update automatically.
  • Required inputs (input.required<T>()) are checked at compile time (NG8008) and have no ! hack; reading one too early throws NG0950.
  • Transforms (booleanAttribute, numberAttribute) and aliases carry over as options.
  • The child cannot write an input signal (no set); for a value the child changes, use model().
  • Timeline: developer preview in 17.1, stable in v19; @Input is still supported (not deprecated in 22.2.3), and a migration schematic exists.

Clarifying questions expected:

  • "Do any of these components assign to their own inputs? Those cannot become plain input()."
  • "Do tests set inputs by assigning the field? They will need fixture.componentRef.setInput()."

Code / implementation expected: Yes — the same component written with @Input() and with input(), ideally with a derived value.

  • inputs
  • signals
  • angular-17

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

11 min read · 12 sections

Read the code

AngularSignal inputs: required, transform, alias and a computed total (needs the Angular compiler)
import { Component, booleanAttribute, computed, input, numberAttribute, signal } from "@angular/core";

// CHILD: every input is a read-only signal the parent writes into.
@Component({
  selector: "price-tag",
  template: `
    <p [class.sale]="onSale()">
      {{ label() }}: {{ total() }}
      @if (onSale()) { <strong>SALE</strong> }
    </p>
  `,
})
export class PriceTagComponent {
  label = input.required<string>();                              // must be bound, or the build fails
  amount = input(0, { transform: numberAttribute });             // accepts "12" from a static attribute
  qty = input(1, { alias: "quantity" });                         // public name differs from the field
  onSale = input(false, { transform: booleanAttribute });        // <price-tag onSale> means true

  // Derived state: no ngOnChanges, no setter. Re-runs only when amount or qty change.
  total = computed(() => {
    const t = this.amount() * this.qty();
    console.log(`  computed total ran: ${this.amount()} x ${this.qty()} = ${t}`);
    return t;
  });
}

@Component({
  selector: "app-root",
  imports: [PriceTagComponent],
  template: `
    <price-tag label="Coffee" amount="3" [quantity]="cups()" onSale />
    <button (click)="cups.update(n => n + 1)">One more cup</button>
  `,
})
export class AppComponent {
  cups = signal(2);
}
AngularThe same component with @Input() and ngOnChanges
import { Component, Input, OnChanges, SimpleChanges, booleanAttribute, numberAttribute, signal } from "@angular/core";

// The SAME price tag written the decorator way (still supported, not deprecated in v22).
@Component({
  selector: "price-tag",
  template: `
    <p>
      {{ label }}: {{ total }}
      @if (onSale) { <strong>SALE</strong> }
    </p>
  `,
})
export class PriceTagComponent implements OnChanges {
  @Input({ required: true }) label!: string;                 // "!" because TS cannot see the parent set it
  @Input({ transform: numberAttribute }) amount = 0;
  @Input({ alias: "quantity" }) qty = 1;
  @Input({ transform: booleanAttribute }) onSale = false;

  total = 0; // derived state has to be kept in sync by hand

  ngOnChanges(changes: SimpleChanges) {
    console.log(`  ngOnChanges: ${Object.keys(changes).join(", ")}`);
    this.total = this.amount * this.qty;
  }
}

@Component({
  selector: "app-root",
  imports: [PriceTagComponent],
  template: `
    <price-tag label="Coffee" amount="3" [quantity]="cups()" onSale />
    <button (click)="cups.update(n => n + 1)">One more cup</button>
  `,
})
export class AppComponent {
  cups = signal(2);
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 42 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextMedium

What is the difference between reactive and template-driven forms?

Continue