The @Input Decorator is Out… So Is ngOnChanges. Now What?

October 25, 2024 | 10 Minute Read

Hey there, Angular folks, and welcome back! If you're still using @Input decorators and ngOnChanges() for managing states, this tutorial is for you! We’ll take two simple forms that are enabled and disabled programmatically based on an @Input and refactor them to use Angular’s latest signal-based approach.

Trust me, it’s easier than you think, and it’ll make your code cleaner, more performant, and more modern!

Understanding the Existing Application: Setting the Stage

Here’s the scenario, I already have a couple of reactive forms set up.

First we have a sign-in-form component where we have an Angular Form Group for our two form fields: “username” and “password”:

protected form = new FormGroup({
    username: new FormControl<string>('', Validators.required),
    password: new FormControl<string>('', Validators.required)
});

We’re using an @Input named “disabled” to accept a boolean value from a parent component:

@Input() disabled = false;

Then, in the ngOnChanges() function, if the @Input is true, we disable the form, if not we enable it:

ngOnChanges(changes: SimpleChanges) {
    if (changes['disabled']) {
        this.disabled ? this.form.disable() : this.form.enable();
    }
}

Also, for the purposes of this example, we are displaying the disabled status of the form in the UI:

Example of the disabled status of the form displayed in the UI

If we switch to the template, down at the bottom, we can see that we’re rendering the word “Disabled” when the Angular Form Group is disabled and then “Enabled” when it’s not:

<footer [class.disabled]="form.disabled">
    Sign In: {{ form.disabled ? 'Disabled' : 'Enabled' }}
</footer>

So, this form is currently enabled, but we’re also seeing this other message where it says, “Sign Up: Disabled”:

Example of the disabled status of the form displayed in the UI

Well, this is because we also have a sign-up-form component, and its form is currently disabled.

So, if we click the “Sign Up” button at the top of the page, the UI switches to show that sign-up form component. And now we can see that the sign-in form component is disabled, and this form is now enabled:

Example of the disabled status of the form being toggled in the UI

If we look at the code for this component, we see a very similar set up, but we only have a single form control for the “name” field instead of a form group like the sign-in component.

Other than that, it’s the same with the “disabled” @Input and the control enabling and disabling within the ngOnChanges() method:

@Input() disabled = false;
protected name = new FormControl<string>('', Validators.required);
protected submitted = false;

ngOnChanges(changes: SimpleChanges) {
    if (changes['disabled']) {
        this.disabled ? this.name.disable() : this.name.enable();
    }
}

Now, in our root app component, we have both of these form components.

The disabled input on the sign-in form will be true when our “formMode” property equals “signUp”:

<app-sign-in-form [disabled]="formMode === 'signUp'"></app-sign-in-form>

Then, on the sign-up form it will be disabled when the “formMode” equals “signIn”:

<app-sign-up-form [disabled]="formMode === 'signIn'"></app-sign-up-form>

When we click on these buttons for the tabs at the top, the value of this “formMode” property is toggled to properly display and enable the appropriate form.

<ul>
    <li>
        <button (click)="formMode = 'signIn'" >
            Sign In
        </button>
    </li>
    <li>
        <button (click)="formMode = 'signUp'" >
            Sign Up
        </button>
    </li>
</ul>

So that’s how it’s currently set up and it all appears to be working just fine as is right?

Well this is true, but it’s not using the latest Angular features like signal inputs, so let’s update it to do so.

Evolving the Angular App: Migrating from @Input Decorators to Signal Inputs

To switch from the @Input decorator, it’s pretty easy, we can just remove the decorator.

Then we can add the input function. We’ll need to be sure to import it from the @angular/core module, and we can set its initial value to false:

import { ..., input } from "@angular/core";

disabled = input(false);

This function provides signals for reactive state management. It’s part of Angular’s move towards a more reactive architecture.

Now at this point, it’s a signal. We just need to add parenthesis to the usage in the ngOnChanges() method:

ngOnChanges(changes: SimpleChanges) {
    if (changes['disabled']) {
        this.disabled() ? this.form.disable() : this.form.enable();
    }
}

But now that it’s a signal, we can change this. We can completely get rid of ngOnChanges.

Instead, we’ll leverage Angular’s new effect() function to monitor for changes to the “disabled” signal input instead.

This simplifies our code and makes it more reactive.

Important Disclaimer About Effects!!!

It’s important to note here that the effect() function is not always going to be the best choice.

You really shouldn’t use the effect() function if you need to update other signals.

If that’s what you need to do you should look into creating computed signals instead.

But for this example, the effect() function works just fine, so we’re going to use it.

Maximizing Performance: Transitioning from ngOnChanges() to Signal Effects in Angular

To switch to the effect() function, we need to first add a constructor.

Then, we can add the effect() function within the constructor.

We’ll also need to be sure that it gets imported from the @angular/core module too:

import { ..., effect } from "@angular/core";

constructor() {
    effect(() => {
    });
}

Now, once we include our “disabled” signal input within this effect() function, whenever the signal value changes, this effect automatically runs. No need to manually check for changes like we did with ngOnChanges().

So now, we can simply move this logic into the effect() function:

effect(() => {
    this.disabled() ? this.form.disable() : this.form.enable();
});

Then we can remove the ngOnChanges() method and its imports too.

And that’s it.

Now this component has been properly updated to use more modern Angular Features. So we should switch over and update the sign-in form component too.

We need to:

  • Switch the @Input decorator to the input function
  • Add the constructor
  • Add the effect() function
  • Move the logic and add parenthesis to the “disabled” signal
  • Remove everything for the ngOnChanges() method and @Input decorator

After all of that, the component should look like this:

import { ChangeDetectionStrategy, Component, effect, input } from "@angular/core";
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";

@Component({
    selector: 'app-sign-in-form',
    templateUrl: './sign-in-form.component.html',
    styleUrl: './sign-in-form.component.scss',
    standalone: true,
    imports: [
        ReactiveFormsModule
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SignInFormComponent {
    disabled = input(false);
    protected form = new FormGroup({
        username: new FormControl<string>('', Validators.required),
        password: new FormControl<string>('', Validators.required)
    });
  
    constructor() {
        effect(() => {
            this.disabled() ? this.form.disable() : this.form.enable();
        });
    }
}

Let’s save and see how this all works now:

Example of the disabled status of the form being toggled in the UI as we switch between the sign-in and sign-up forms

Ok, everything looks good right?

The sign-in form is enabled, and the sign-up form is disabled.

Then when we switch to the sign-up form, it’s enabled, and the sign-in form is disabled.

So, it works exactly like it did before but now it’s up to date with current best practices.

In Conclusion

To recap, we’ve modernized our Angular form components by converting the @Input decorator to the new input function, and we used the effect() function to replace ngOnChanges() in this case.

Our forms will continue to react automatically to input value changes, but will now do so using signals, making the code a little cleaner and more performant.

With just a few lines of code, we’ve refactored our Angular components to use the latest reactive patterns with signal inputs and effects.

Alright, I hope you found this tutorial helpful!

Don’t forget to check out my other Angular tutorials for more tips and tricks.

Additional Resources

Want to See It in Action?

Check out the demo code and examples of these techniques in the in the Stackblitz example below. If you have any questions or thoughts, don’t hesitate to leave a comment.