Don't use @Input().

Use input<T>() (aka Signal Inputs) instead.

How

export class UserProfileComponent {
  @Input()
  userId: string;
}

becomes

export class UserProfileComponent {
  userId = input<string>();
}

Why

Cleaner and less bug-prone code

In old code, derived properties needed to be updated with lifecycle hooks. Consider the following extension to the above example:

export class UserProfileComponent {
  @Input()
  userId: string;
  
  user: User;
  
  userDataService = inject(UserDataService);
  
  ngOnInit() {
    this.user = this.userDataService.getUser(userId);
  }
}

What if userId changes? Use the OnChanges lifecycle hook:

export class UserProfileComponent implements OnChanges, OnInit {
  @Input()
  userId: string;
  
  user: User;
  
  userDataService = inject(UserDataService);
  
  ngOnInit() {
    this.user = this.userDataService.getUser(userId);
  }
  
  ngOnChanges(changes) {
    if (changes.userId) {
      this.user = this.userDataService.getUser(userId);
    }
  }
}

The ngOnInit block can be omitted (OnChanges is triggered after OnInit). There are still two problems:

  1. Many developers don’t realize that.

  2. It’s imperative which can be harder to understand than declarative style.

  3. As components grow large in internal state this approach scales together in complexity.

Using input signals is better:

export class UserProfileComponent {
  userId = input<string>();
  userDataService = inject(UserDataService);
  user = computed(() => this.userDataService.getUser(this.userId()));
}

This is clean and concise. When userId changes, user will be updated automatically. Note: using this component is exactly the same in both cases:

<user-profile userId="currentUserId"></user-profile>

Performance

Signals are faster and will cause less dirty checking in the Angular component tree.