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 and explain why it works the way it does.

FormControl.disable triggers valueChanges Observable

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:

@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:

this.control.disable({emitEvent: false});

Inheriting Input/Output properties

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:

export 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:

export 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 the inputs property; you can disable the warning with tslint:disable:use-output-property-decorator comment

ngOnChanges vs ngOnInit

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 Forms

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 strings

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:

const 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:

class 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.

Conclusion

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.