DEV Community

Cover image for Be ready for input signals in Angular
Nicolas Frizzarin for This is Angular

Posted on

Be ready for input signals in Angular

Introduction

With the release of Angular 17, the signals have become stable.

As a reminder, a signal is a wrapper around a value that can notify interested consumers when that value changes. Signals can contain any value, from simple primitives to complex data structures.

At present, the core team has only delivered the basic signals api. But its long-term goal is to go much further, and achieve signal change detection.

The next step is to have input and output based on signals. And this functionality should appear very soon this month with version 17.1.

How can we prepare for this functionality to facilitate the migration of our current Input and Output?


A litte reminder

Before getting straight to the heart of the matter, a quick reminder may be in order.

A signal is created using the signal function.



const name = signal<string | null>('DevTo');


Enter fullscreen mode Exit fullscreen mode

This function returns a WritableSignal, enabling us to modify the value of our signal using the methods:

  • set
  • update


const count = signal(0);
count.set(10);
count.update(count => count + 1);


Enter fullscreen mode Exit fullscreen mode

A signal can also be made readonly using the asReadonly function.



const count = signal(0);
const readonlyCount = count.asReadonly();


Enter fullscreen mode Exit fullscreen mode

Why passing a signal as an input is a very bad idea ?

Working with signals in a real application can sometimes become very complicated, especially with components that take inputs.

As a reminder, modifying change detection in OnPush mode with signals improves performance, especially page refresh speed.

When a signal is used in the template of an OnPush component, Angular automatically adds it as a reactive dependency of the component, and each time this signal is modified, Angular will automatically mark the component as dirty, which will ensure that the component is refreshed the next time the change detection is run.

Now let's imagine two components with a parent-child relationship




@Component({
  selector: 'app-father',
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  imports: [AppChildComponent],
  template: `<app-child [name]="name" />`
})
export class AppFatherComponent {
  name = signal('DevTo');
}


@Component({
  selector: 'app-child',
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  template: `{{ name() }}`
})
export class AppFatherComponent {
  @Input({ required: true }) name !: WritableSignal<string>
}


Enter fullscreen mode Exit fullscreen mode

Passing a signal as input to a child component have several problems:

  • unidirectional binding is lost
  • you force the parent to use signals
  • you lose the granularity of the detection change

Unidirectional binding and granularity of the detection change

When we pass a signal as input, we don't pass the value of the signal but actually its reference.

It therefore becomes normal that if the signal is modified in the child, the parent is also impacted, so a detection change cycle will have to be lifted in both the parent and the child.

An Angular application is nothing more than a tree of components, so by passing signals as input, no matter whether the component is OnPush or not, each modification of the signals in the child components will automatically trigger a change of detections in the parent, thus losing the added value of the OnPush mode.

What's more, by passing a signal as input, the logic of data centralization is totally lost, as any descendant of a parent component can modify the variable, and in the event of a bug we end up with a spaghetti node that's very complex to debug.

Force the parent to use signals

One of the workarounds used to avoid the above problems would be to switch the signals to readonly mode.

Apart from being unsightly and an anti-pattern, this would force the parent component to use signals. In other words, all inputs passed to child components must be signals.

Which makes no sense, as the parent component should be able to pass simple variables to its children.
What's more, when input signals arrive, all the code will have to be refactored.


Input as a setter for the win

In Angular, inputs can be setters. This will help you to create signal-based inputs.

This technique will avoid the problems analyzed above, but there may well be a few more boilerplates while we wait for release 17.1.

The idea is to transform all our Inputs into an aliased Input setter and create the signal associated with this input.

Let's take the example above




@Component({
  selector: 'app-father',
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  imports: [AppChildComponent],
  template: `<app-child [name]="name" />`
})
export class AppFatherComponent {
  name = signal('DevTo');
}


@Component({
  selector: 'app-child',
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  template: `{{ name() }}`
})
export class AppFatherComponent {
  @Input({ required: true, alias: 'name' }) set _name(name: string) {
    this.name.set(name);
  }
  name = signal<string>('');
}


Enter fullscreen mode Exit fullscreen mode

With this method, the parent component is totally flexible on the type of variable passed to child components, unidirectional binding is respected and migration to signal inputs is simplified.

In fact, with version 17.1, all you need to do is remove the input setter and replace the signal function with the input function


Inputs Signal

Input signals are declared using the input function

This function not only creates readonly signals, but also ensures that the metadata passed to the input annotation is not lost.



const name = input<string>(''); // input with default value
const name = input<string>(); // input with no default value
const name = input.required<string>() // input mandatory
const name = input<string>('', { alias: 'lastname' }); // with alias
const isLoading = input<string | boolean; boolean>('', { transform: booleanAttribute });


Enter fullscreen mode Exit fullscreen mode

If we go back to our previous example, the code would be as follows:




@Component({
  selector: 'app-father',
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  imports: [AppChildComponent],
  template: `<app-child [name]="name" />`
})
export class AppFatherComponent {
  name = signal('DevTo');
}


@Component({
  selector: 'app-child',
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  template: `{{ name() }}`
})
export class AppFatherComponent {
  name = input.required<string>();
}


Enter fullscreen mode Exit fullscreen mode

What will happen in the future ?

the plan

Based on this shema, reactivity and change of detection within Angular will take place around signals.

In a few months' time, we hope to be able to do without ZoneJs with OnPush components that use signals, as well as creating components based signals like this



@Component({
  selector: 'app-father',
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  signals: true,
  imports: [AppChildComponent],
  template: `<app-child [name]="name" />`
})
export class AppFatherComponent {
  name = signal('DevTo');
}


Enter fullscreen mode Exit fullscreen mode

Top comments (5)

Collapse
 
teej107 profile image
teej107

Input signals look pretty cool but what would the advantage be using them over plain input decorators? The only advantage I can think of is that input signals are readonly.

Other than that, Angular triggers change detection when inputs change anyway so it seems like whether to use plain input decorators, inputs as setters, or input signals doesn't matter anyway.

Collapse
 
premkumarfrombosch profile image
prem-kumar-from-bosch

Yes the change is detected when plain input but how about objects and arrays?
This should remove dependency on ngonchanges previous and current thing

Collapse
 
nicojs profile image
Nico Jansen

I think some of the code examples have a small typo. Shouldn't the signal in the parent be executed when a string is expected in the child component?

template: `<app-child [name]="name()" />`
Enter fullscreen mode Exit fullscreen mode
Collapse
 
artydev profile image
artydev

than you :-)

Collapse
 
jangelodev profile image
João Angelo

Hi Nicolas Frizzarin,
Your article is very cool
Thanks for sharing