Angular: The Unexpected
When your favorite framework doesn’t work as you thought it does. Every Angular developer has encountered some instances when the framework did something unusual, and sometimes even outright nonsensical.

Angular: The Unexpected
When your favorite framework doesn’t work as you thought it does. Every Angular developer has encountered some instances when the framework did something unusual, and sometimes even outright nonsensical.


Today we are going to take a look at some of those cases when the framework did something unusual and explain why it works the way it does.
FormControl.disable
triggers valueChanges
ObservableLink to this section
The problem:
When using Reactive Forms the default way of disabling a FormControl
is by using the methods enable
and disable
. Let’s take a look at this example:
<>Copy@Component({ selector: 'my-component', template: ` <input [formControl]="control"> <button (click)="toggleEnabledState()">Toggle State</button> `, }) export class MyComponent implements OnInit { control = new FormControl('Default Value'); ngOnInit() { this.control.valueChanges.subscribe(console.log); } toggleEnabledState() { this.control.enabled ? this.control.disable() : this.control.enable(); } }
Now this is simple: we have an input element bound to a FormControl
, with a “Default Value”, we listen to the valueChanges
Observable
, and then there is a button that toggles the enabled/disabled state of the control. What’s the problem? Well, if we click several times on the button, and then look in the console, we will see that the “Default Value” has been logged every time we clicked, event though the value hasn’t technically changed. This may be especially confusing and hard to find out when we listen to changes on a large form and enable/disable some of the nested controls based on user permissions/preferences. We would probably put the subscribe
inside our ngOnInit
lifecycle hook, but the enabling/disabling preferences may be loaded via an HTTP call, and arrive later than the subscription is started, resulting in unwanted emissions.
The reasoning:
Okay, while technically the value itself hasn’t changed, if the control is a part of a larger FormGroup
or FormArray
it being enabled or disabled will change the value of the parent FormGroups
, removing the disabled value, and seeing as a FormGroup
‘s valueChanges
is the combination of its child controls’ valueChanges
, the child controls have to fire.
The solution:
Just call enable
/disable
with an emitEvent
argument set to false
:
<>Copythis.control.disable({emitEvent: false});
Inheriting Input/Output propertiesLink to this section
The problem:
In my article about using TypeScript Mixins in Angular I advocated that sometimes inheriting from a base “component” class can be useful. But there is a situation where one particular feature of Angular won’t work as intended:
<>Copyexport class BaseComponent { @Input() something = ''; } @Component({ selector: 'my-selector', template: 'Empty', }) export class InheritedComponent extends BaseComponent { // actual class implementation }
As you see, we inherited our component from a base class, but the base class also has an Input
property, nd that is catch: when we run our app, it will work fine, but the production build will fail, claiming that our inherited class component has no Input
called “something”.
The reasoning:
This is not something done on purpose by the Angular team, but rather a bug, as seen in this GitHub issue.
The solution:
Declaring input properties on the child component’s decorator explicitly:
<>Copyexport class BaseComponent { @Input() something = ''; } @Component({ selector: 'my-selector', template: 'Empty', inputs: ['something'], }) export class InheritedComponent extends BaseComponent { // actual class implementation }
We just told the Angular compiler explicitly that the inherited property is an Input
. This is a temporary workaround until the Angular team fixes the compiler issue.
Note: TSLint is going to complain about theinputs
property; you can disable the warning withtslint:disable:use-output-property-decorator
comment
ngOnChanges
vs ngOnInit
Link to this section
The problem:
We use lifecycle hooks like ngOnInit
and ngOnChanges
on a near daily basis, and usually we think that they are simple enough for us to be sure when exactly each on of them works. And, because ngOnInit
is called, well, “on init” we might assume it works first of all. But that is simply not the case! Sometimes ngOnChanges
can be invoked sooner than ngOnInit
; as a matter of fact, that is what usually happens!
The reasoning:
The thing is, ngOnInit
is supposed to work when the view starts rendering; on the other hand ngOnChanges
is supposed to work whenever the Inputs
change; and when we put it like this, it becomes apparent that in no way is ngOnChanges
required to wait for ngOnInit
. Very often inputs of a component can be changed from the parent component while it is only preparing to be rendered (thus, before ngOnInit
).
The solution:
If there is any logic inside ngOnInit
that depends on some data parsed inside ngOnChanges
, we have to give careful thought to how we organise our code. This especially concerns the cases when we listen to FormControl.valueChanges
and perform something based on changed component inputs.
Non-type-safety of the Reactive FormsLink to this section
The problem:
In my latest article on Angular Forms I mentioned that one problem with Angular Reactive Forms is that they don’t provide type safety which is so essential for projects written in TypeScript. This may result in hindered IDE experience, typos that cannot be caught, and some repetitive code.
The reasoning:
The main problem with Reactive Forms that they are very customizable and probably cannot be kept entirely type safe
The solution:
We can stick to the solution I provide in my article mentioned above, or come up with a more custom solution, like using a wrapper around ReactiveForms or some OOP machinations.
NGRX Action types are (not) unique stringsLink to this section
The problem:
If you have used Angular for while, chances are you are familiar (or even already using) the Angular state management library NGRX. I won’t go into much detail, but one thing that developers often overlook (and then are surprised to discover) is that Actions are uniquely identified by the type we provide. To understand how being careless with this can lead to sometimes catastrophic results, let’s take a look at this piece of code:
<>Copyconst loadData = createAction('[Home Page] Load Data'); const loadDataSuccess = createAction( '[Home Page] Load Data', props<{payload: object}>(), ); const _dataReducer = createReducer( {}, on(loadDataSuccess, (state, {payload}) => ({...state, ...payload})), ); @Injectable() export class DataEffects { loadData$ = createEffect(() => this.actions$.pipe( ofType(loadData), mergeMap(() => this.dataService.getData().pipe( map(payload => loadDataSuccess({payload})), )), )); constructor( private readonly actions$: Actions, private readonly dataService: DataService, ) {} }
So, essentially this code boils down to this: wait for a loadData
action to be dispatched; when it is, call a data service API, load some data, and when the data is ready, dispatch another loadDataSuccess
action which will put the received data into the store (error handling is omitted for brevity). When we open our application, we will see that everything worked fine, out data is loaded and displayed just fine. So what’s the catch? It will become apparent if we open the “Network” tab in our browser — and behold — the data API is being called infinitely many times! Somehow, our Effect
got stuck in an infinite loop. Okay, but how? And what should I do?
I find this one a particularly hard problem to debug, it took me three days before I realised what was the matter — and the thing is, when we call createAction
we accidentally put the same type string for both loadData
and loadDataSuccess
! The problem is that NGRX relies on the type property to determine which action it is, and, well, because we provided the same type for two different actions, the Effect
got triggered when the action was dispatched, and then sort-of dispatched the same action (even though under a different name).
The reasoning:
It is way easier to rely on the provided type for identification of action than to come with a contrived name-system, so NGRX team went with it (it will change though soon — see the solution section)
The solution:
We can either be extra careful when declaring actions, and every time we encounter a strange NGRX bug just start from checking action type strings, or we can come up with something like this:
<>Copyclass ActionNames { private static names = new Set<string>(); static create(name: string): string { if (ActionNames.names.has(name)) { throw new Error('An Action with this type already exists!'); } ActionNames.names.add(name); return name; } } const someAction = createAction(ActionNames.create('[Page] Actions Name'));
This will check if we redeclare the same action type and just throw an error, preventing bugs like these from happening in the first place. I personally think that this solution is a little bit contrived and unnecessary, and it is better to just be more attentive and prepared. But if you have a large codebase and a not very experienced team, and bugs like this one happen a lot, you can use this solution just fine.
But there are good news, we can also use the latest version of NGRX which includes this solution implemented by the NGRX team itself — runtime checks for action type uniqueness. This will throw an error when an action with an already registered type is created. And until we update, we can also use these TSLint rules which also include checking for action type uniqueness.
ConclusionLink to this section
Angular is a vast ecosystem of interconnected features, and even though we may wrongfully assume we know perfectly how it works, it still may come up with surprised. It is always better to learn from mistakes, and knowing where other developers have stumbled can help you prevent bugs in your own code and save you lots of time.
Comments (0)
Be the first to leave a comment
About the author

Senior Angular developer from Armenia. Passionate about WebDev, Football, Chess and Music

About the author
Armen Vardanyan
Senior Angular developer from Armenia. Passionate about WebDev, Football, Chess and Music
About the author

Senior Angular developer from Armenia. Passionate about WebDev, Football, Chess and Music