FormControl.disable fires the valueChanges Observable

The issue:

In Reactive Forms, the standard approach for toggling a control’s availability is through the enable and disable methods. Consider this scenario:

@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();
  }
}

The setup is straightforward: an input field tied to a FormControl holding a “Default Value”, a subscription to the valueChanges Observable, and a button that switches the control’s state. The caveat appears when the button is pressed repeatedly. The console will show “Default Value” being logged on each click, even though the actual value remains unchanged. This can be particularly tricky to trace in larger forms where controls are being enabled or disabled conditionally based on user roles or preferences. Typically, the subscribe call would live inside the ngOnInit lifecycle hook, but if those enabling/disabling preferences are fetched asynchronously via an HTTP request, they might arrive after the subscription is active, causing these unexpected emissions.

The rationale:

While the control’s value itself hasn’t changed, its enabled/disabled status does have an impact if it lives within a parent FormGroup or FormArray. Toggling the state will alter the parent FormGroup's value by omitting the disabled entry. Since a FormGroup's valueChanges is a merge of its children’s streams, each child control must emit to keep that aggregate stream coherent.

The fix:

Pass emitEvent: false to the enable or disable call:

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

Input and Output property inheritance

The issue:

In a prior piece on TypeScript Mixins in Angular, the usefulness of inheriting from a base class was highlighted. However, there’s a scenario where this approach hits a snag:

export class BaseComponent {
  @Input() something = '';
}

@Component({
  selector: 'my-selector',
  template: 'Empty',
})
export class InheritedComponent extends BaseComponent {
  // actual class implementation
}

The derived component extends the base, but that base class holds an Input property. That’s where the problem lies: in development mode things run smoothly, but a production build will fail with an error stating the derived component lacks an Input named "something".

The rationale:

This isn't an intentional design decision; rather, it's a known compiler bug documented in this GitHub issue.

The fix:

List the input properties explicitly in the child component’s decorator:

export class BaseComponent {
  @Input() something = '';
}

@Component({
  selector: 'my-selector',
  template: 'Empty',
  inputs: ['something'],
})
export class InheritedComponent extends BaseComponent {
  // actual class implementation
}

This approach explicitly tells the Angular compiler that the inherited attribute is an Input. It’s a stopgap until the compiler issue gets patched.

Caveat: TSLint may flag the inputs usage; suppress it with the tslint:disable:use-output-property-decorator comment.

The order of ngOnChanges and ngOnInit

The issue:

Lifecycle hooks such as ngOnInit and ngOnChanges are part of our daily routine, and they feel intuitive enough to know when each fires. Given the name, it’s tempting to assume ngOnInit runs first. That assumption, however, is often wrong! In many cases, **ngOnChanges** is executed before **ngOnInit**.

The rationale:

The purpose of ngOnInit is to signal the start of view rendering; ngOnChanges, on the other hand, is meant to fire whenever Inputs are altered. Looked at from that perspective, it makes sense that ngOnChanges doesn't wait around for ngOnInit. Parent components often modify the inputs they bind to a child before that child is actually rendered, so those changes are processed beforehand.

The fix:

If logic inside ngOnInit relies on data that gets processed in ngOnChanges, code organization needs careful consideration. This is especially relevant when subscribing to FormControl.valueChanges in ngOnInit and taking actions based on inputs that have already been modified.

The lack of type safety in Reactive Forms

The issue:

In a recent article on Angular Forms, the point was raised that Reactive Forms lack the type safety that TypeScript projects value. This gaps leads to a poorer IDE experience, allows typos to slip through unnoticed, and often results in boilerplate code.

The rationale:

The core challenge is that Reactive Forms are extremely flexible, which makes it difficult to impose strict type checks without losing that adaptability.

The fix:

Adopt the strategy suggested in that earlier article, or devise a bespoke approach—like creating a typed wrapper or applying some object-oriented techniques.

NGRX Action types as (not so) unique strings

The issue:

Anyone who has spent time with Angular is likely aware of (or using) the state management library NGRX. Without diving too deep, there’s an often-overlooked detail: actions are identified purely by the string provided as their type. Being careless with this can produce unforeseen—and sometimes severe—results. Look at this example:

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,
  ) {}
}

What this code does is straightforward: it listens for a loadData action, makes a service call to fetch data, and upon completion dispatches a loadDataSuccess action to store the result (with error handling removed for simplicity). On initial load, everything appears fine—data shows up correctly. The problem surfaces when inspecting the browser’s “Network” tab: the API is being hit in an endless loop! The Effect has fallen into an infinite cycle.

Tracking this down is notoriously difficult; it took me three days to figure out the root cause—in the call to createAction, both loadData and loadDataSuccess accidentally received the same type string. NGRX depends on that type to distinguish actions; since they were identical, the Effect that was meant to react to the load action got re-triggered by its own success response, endlessly dispatching the same (though differently named) action.

The rationale:

Using a straightforward string as the identifier is far simpler than devising a complex nomenclature system, so NGRX went with that approach (though this is expected to evolve in the future—see below).

The fix:

Option one is vigilance: double-checking type strings whenever an odd NGRX bug surfaces. Alternatively, a more programmatic safeguard can be implemented:

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 utility detects if an action type is being reused and throws an error right away, preventing this class of bugs from occurring. Personally, I find the latter somewhat heavy-handed and prefer being attentive, but in a larger codebase with a less experienced team, the defensive check can be a lifesaver.

There’s also some encouraging news: recent NGRX versions include a runtime check for action type uniqueness natively. It throws on attempting to create an action with a already-registered type. Until migrating, these TSLint rules can also enforce type uniqueness.

Wrapping up

Angular is a large ecosystem where numerous pieces interact. Even when we believe we fully grasp its behavior, it finds ways to surprise us. Learning from these pitfalls—both your own and others—is invaluable; knowing where things can go wrong helps you avoid similar bugs and saves significant debugging time down the road.