Watch out for Inputs in Angular

Picture of a mountain.
Photo by Willian Justen de Vasconcellos on Unsplash

One of the most popular methods of communication between components is by inputs. In the community, they are known for being easy to use, understand and implement. Is this really true?

I guess you must have heard the practice:

Don’t use inputs with setters.

What about input initialization in relation to the constructor or any life cycle hook? When the value on the input property appears and is ready to be used?

Another issue with inputs happens when you start to work on a project with enabled TypeScript strict mode. You find yourself asking: How to declare an input property so the compiler will not report any errors?

These are the questions that often appear when you work on an Angular project. In this article, I will try to show you some of the aspects of Angular mechanics that lack of knowledge may bring issues to the developers.

Angular Component

I do get asked a lot about what is my favorite part of Angular and I always answered with: the Dependency injection mechanism. Recently I came to the conclusion that there is one part of Angular which I always overlooked and never appreciated enough and it’s the Component.

It is very powerful and flexible. It allows to build amazing applications, use different patterns and create architectures unique for each solution.

With all of that said I must say I question a couple of design choices which Angular team took. After working for a couple of years with the framework in different projects I can tell that I see a pattern of issues that comes with a couple of features, mostly regarding decorators:

  • Inputs — also known as @Input,
  • Queries — @ViewChild, @ViewChildren, @ContentChild, @ContentChildren,
  • @HostBinding and @HostListener.

In this article, I will cover inputs.

Inputs

Inputs are a great method of communication.

They are really straightforward. Simple and easy-to-use way of passing data to the component.

Basic input usage
Basic input usage

Code is self-explanatory. If you are interested to read more I suggest A deep dive into Angular Inputs.

IoC

One of the problems that I find is that Input properties are controlled by the framework. The developer creates a property in a component and decorates it with an @Input(). Then the framework magically sets property value on a specific life cycle hook. There is a lot going on with such a simple mechanism 🤔.

Let's take a look a the example code, that will not work, can you already see the problem?

UserComponent with a UserService
UserComponent with a UserService

The input value is used in the constructor. At this point of a components life cycle, the value of the property will be undefined. You need to wait for the ngOnChanges life cycle hook in order for the value to appear.

The correct way of reacting to the input value change
The correct way of reacting to the input value change

As a developer, you have to put effort into thinking and knowing when you can access input value and when it is not ready. If you are an inexperienced developer you will definitely make this mistake, I know because I did it many times.

Setters

The use case presented in the previous paragraph showed an example of static reading and accessing input property. Sometimes you want to dynamically respond to a situation in which input changes its value. E.g. invoke a method after the value of an input changes. Many guides recommend using a combination of a setter and an input, just like that:

The input used with a setter method
The input used with a setter method

This feature has another hidden mechanic.

Input setters are invoked in the order of declaration in the component.

In the example below you have two setters. The first value of users will be set and then avatars.

Input setters with relations to each other
Input setters with relations to each other

The avatar property is undefined at the moment of the setting user value. It’s a common mistake. Putting any code where the order of setter matters will break your application.

I have seen a couple of times when this hidden feature created a bug. That is another framework-specific behavior that developer needs to know. It would be nice for the framework to not put developers in such situations.

strict mode

Everyone who works on an Angular project which has the strict mode turned on has seen this error:

Simple UserComponent with one input
Simple UserComponent with one input
Error message Property has no initializer and is not assigned in the constructor
Error message Property has no initializer and is not assigned in the constructor

It comes from the check called strictPropertyInitialization. It requires that every property in a class must be initialized with a value. So the easiest solution is to specify the default value.

Unfortunately, sometimes you cannot do it as you don't know the userId or a taskAssignee. In this situation, you have to rely on a style guide and practices like:

  • optional properties — aka the question mark next to the field ?,
  • required input — exclamation mark !,
  • declare the type of property as null —this solution is the worst.

You can read more about the strict mode in this article: Bulletproof TypeScript — strict mode.

If you are interested in an Angular style guide that helps to deal with these issues I recommend reading: Angular component practices.

Alternative Input API

I must say I spent some time dealing with the presented issues and I came up with an interesting conclusion. Maybe it will inspire some of you to also think about the framework API and what we can achieve with it. With all that said take a look at my proposition of different approach to the input declaration:

UserComponent with alternative API
UserComponent with alternative API
UserComponent usage in a template
UserComponent usage in a template

Let’s start by explaining what is going on. Instead of creating an input with a decorator @Input you do it with a function createInput. Then you can use the input's value by subscribing to an observable representing stream of changes returned by the method on. You can also read inputs value in a static way using the method get:

Input usage in a template
Input usage in a template

You can find a working example of this code:

One disadvantage of this solution is that you need to specify inputs name in the component metadata inputs: ['user'].

Overview

The createInput function can be used as an alternative to the Angular @Input. It creates an object of the type ReactiveInput which represents the input.

ReactiveInput type definition
ReactiveInput type definition

The ReactiveInput has two methods: on and get. The first one returns an observable that provides a stream of input values. Every time it changes observables emits a new value. The second method get is for statically access input value, but I don’t recommend using it as an input that may change unexpectedly.

Accessing reactive input value in the template is fairly easy, you just have to use async pipe:

Input usage in a template with an async pipe
Input usage in a template with an async pipe

Observe changes

One of the main benefits of this solution is easy access to input changes. When you want to observe changes in input, instead of using ngOnChanges a life cycle hook or setter, you can use a simpler approach. Basically, you can subscribe to a stream of changes as an object provides a method for that:

RequiredInput used in a constructor
RequiredInput used in a constructor

You don’t have to think about which method to choose or when the value appears during the component's life cycle. Just subscribe to the stream that’s it. You are able to do it in every part of components code, instead of relying on the exact component life cycle.

Property initialization

The created input property is initialized at the moment of declaration, so you will get no issues regarding strict mode. You will be forced to put unnecessary ifs in your code in order to always be sure that the value appears.

Angular input is a reactive mechanism. Its value may change during the component's life and from the component level, you cannot be sure when it happens. This behavior is the definition of a stream and in my opinion, an observable should be used to represent the value of and input. I understand back when Angular was designed there were a few of us who were familiar with the concept of observables. Now it's 2022 and RxJS is very well known by Angular developers.

Implementation

Let’s take a look at how the createInput function works.

export function createInput<T>(inputName: string, component: any): ReactiveInput<T> {

	let value: T | undefined = undefined;
	const values$ = new Subject<T>();

	if (!components.has(component)) {
		component.__proto__.ngOnChanges = (changes: SimpleChanges) => {
			const changeFunctions = changeFunctionMap.get(component) || [];
			changeFunctions.forEach((changeFunction: changeFunction) => {
				changeFunction(changes);
			});
		};
		components.add(component);
		changeFunctionMap.set(component, []);
	}

	changeFunctionMap.get(component)!
					.push((changes: SimpleChanges) => {
						if (changes[inputName]?.currentValue) {
							value = changes[inputName]?.currentValue;
							values$.next(changes[inputName]?.currentValue);
						}
					});

	return {
		get: () => value,
		on: () => {
			return values$;
		}
	};
}

The function returns an object which allows to access the static value of an input or an Observable. In order to be able to get the input’s value, it overrides the ngOnChanges method and every time request input changes it emits a new value.

The code is available on github, you can play around with it.

Solution summary

Pros:

  • Clean readable API,
  • Easy access to input changes,
  • Property created with the method createInput is initialized at the start, so no unwanted ifs in your code,
  • access to input in every part of the component.

Cons:

  • You need to declare input in inputs. Maybe there is a hack that will let you do it differently, but I haven’t found it,
  • It doesn't work in a zoneless environment, as the async pipe doesn't work when zones are disabled. You could use e.g. rxLet instead, from library rx-angular.

I hope I was able to present to you my concept for a different approach to Angular inputs. I highly recommend playing around with it. Maybe you will be able to improve it or maybe you will come up with a different concept for another issue you are facing in your project. The presented alternative API shows that Angular is a great flexible framework and it can be used in many different ways. The most important thing is to keep an open mind and do not close to other solutions to everyday situations.

Conclusions

Unfortunately for now I don’t have perfect solutions for the issues that I have presented. One of them is to build a team with experienced developers who spread the knowledge of Angular across the whole organization. I guess you can also spot many of these issues during code review. Some of them can be resolved with conventions, practices and a style guide.

In this article, I wanted to show that Angular is a great framework. Still, it can improve on lots of mechanics. I feel that decorators are too magical as you as a developer leave too much control to the framework. I hope that after reading this article you feel inspired to look for different usages of Angular mechanics.

Please share your opinion with me on what you think about decorators for properties in Angular components.

Luke, Software engineer, passionate about best practices, frameworks React, Vue, patterns and architectures for modern web
				development, open-source creator, TypeScript enthusiast, Generic UI Technical Blog

Luke

Software engineer, passionate about best practices, frameworks React & Vue, patterns and architectures for modern web development, open-source creator, TypeScript enthusiast.