Inputs don’t always work as expected in Angular.

Even now, quite a few years after the Angular 2 breakdown, I hear and see in action a misconception about Angular inputs and lifecycle.

How does it read ? Something like that :

The OnInit Angular lifecycle method is running when inputs have a value, so you should not read inputs values in the constructor, but instead, in the ngOnInit method.

But honestly, this statement might not reflect properly how things actually work.

When I do code reviews, I see that this assumption leads to additional problems. People assume that the code that handle changes can only be declared in the ngOnInit method. An untrained developer will assume this scenario quite often. However, If we are talking about a simple component where the value is hardcoded, it will be ok to have it declared there.

export class ParentComponent {
  pouf = 'pouf';
}

<app-child [pouf]="pouf" />

export class ChildComponent implements OnInit {
  @Input({ required: true })
  pouf!: string;

  ngOnInit(): void {
    // here you have your input value
    console.log(this.pouf);
  }
}

Handle inputs when you need them

It has been written quite a lot of articles about how to manage inputs, the different methods, using a setter and a getter methods … and so on.

Detecting @​Input changes in Angular with ngOnChanges and Setters - by Todd Motto

I will just give my two cents here, as why we have to do that.

We take the previous example and instead of a simple value, we produce an Observable and give it to the child component.

export class ParentComponent {
  title$ = of('Alain').pipe(delay(2000));
}
<app-child [title]="(title$ | async)!" />

export class ChildComponent implements OnInit {
  
  @Input({ required: true })
  title!: string;

  ngOnInit(): void {
    //Here you have null in your input
    console.log(this.title);
  }
}
  • We made a delayed Observable to simulate some kind of API call
  • We see that there is no value when calling this.title in the ngOnInit method

So no, you simply can’t rely on this method to work with your inputs. So yes, every time we can, we should work on a reactive way with our data, to avoid that kind of trouble.

Adding the OnChanges interface will allow us to monitor this data and work only when it’s really available :

ngOnChanges(changes: SimpleChanges) {
  if (changes["title"]) {
    //I know I have the data
    console.log(this.title);
  }
}

Working asynchronously with Inputs

You will need to work with the asynchronous data in different ways. One of the best solution I’ve been trying, was to make these Inputs become Observables. In this article, you create an annotation for that purpose :

How to build reactive Angular Components using Inputs as Observables

The result in your code will look like this (taken from the above article) :

@Component({
  selector: "my-component",
  template: `<p>result: {{ result$ | async }}</p>`,
})
export class MyComponent {
  @Input() prop!: number;

  @Observe("prop") private prop$!: Observable<number>;

  result$ = this.prop$.pipe(
    switchMap((prop) => this.myService.getResult(prop)),
    // share the response across all subscribers to prevent
    // multiple HTTP requests
    share()
  );
}

This code is hiding the complexity of transforming the input into an Observable. You would not have to wonder if the data is available when you need it. You simply write reactive code.

Working with Angular Signal Inputs

But the clever reader will say that of course we don’t use the old annotation API and instead we should use the Signal Inputs. Let’s do that, with having still an Observable as the source of data.

export class ParentComponent {
  meuh$ = of('meuh').pipe(delay(3000));
}
<app-child [meuh]="(meuh$ | async)!" />

export class ChildComponent implements OnInit, OnChanges {
  meuh = input.required<string>();

  constructor() {
    effect(() => {
      //First run: this input is null
      console.log('Effect : ', this.meuh());
    });
  }

  ngOnInit(): void {
    //Here you have null in your input
    console.log(this.meuh());
  }
}
  • The nature of the data doesn’t change the fact that its source is an Observable. We simply can’t assume the value in the child component, wether it’s a regular Input or a Signal Input.
  • We still can use the OnChanges method to monitor and use this value :
ngOnChanges(changes: SimpleChanges) {
  if (changes["meuh"]) {
    //I know I have the data
    console.log(this.meuh());
  }
}

Of course we probably would not use an Angular Signal Input this way, but instead, we would use some reactive code, based on effect and computed.

Derive Inputs data

Since we want to use the data of our Inputs and transform it, what options do we have ?

Considering we don’t want to use the ngOnInit method, we could use the setters, with some issues that you can read about here. Namely, you can work safely on one Input with a setter, but as soon as there are more than one, you start having troubles knowing when all inputs are available.

Working with the ngOnChanges method like mentioned above is a good solution, but it’s not the most elegant one. You could use the changes object, but you only have one Input at a time. Yet, you could use it like this :

export class UserComponent implements OnChanges {
  @Input({ required: true })
  name!: string;

  @Input({ required: true })
  id!: number;

  fullName?: string;

  ngOnChanges(changes: SimpleChanges): void {
    // I could use changes['id'] but then I would need to be sure 
    // that the other Input has value
    if (this.id && this.name) {
      this.fullName = this.id + ' - ' + this.name
    }
  }
}

We can see that this is pretty "imperative" code, meaning we make an action when something happens. Using the signals API, we could have a more simple way to use these non synchronous Inputs. This way would be much more "reactive" (and less verbose too) :

export class UserSignalComponent {
  id = input.required<number>();
  name = input.required<string>();
  fullName = computed(() => (this.id() && this.name()) ? 
    this.id() + ' - ' + this.name() : 
    '');
}

Conclusion

If there is a takeway in this article it would be :

Do not rely on Angular OnInit lifecycle method to handle your inputs data in your components.

Here is some Stackblitz : https://stackblitz.com/edit/stackblitz-starters-38fyzb

Check this great article from Angular University about the Signal Components in Angular :

Angular Signals Component API: input, output, model (Complete Guide)


Tagged in:

Angular 17, Inputs, Lifecycle

Last Update: July 01, 2024