Menu

Angular forwardRef

May 28, 2021 by Christopher Sherman

Angular’s forwardRef wraps the provided class in a closure, allowing the provided class to be updated via memory reference once it is interpreted. We use a forwardRef when we need to provide a class before that class is actually declared in code.

JavaScript classes are actually JavaScript functions, and Angular assigns these functions to variables. These variables get hoisted like any other variable, so when a variable is referenced before it is defined, it has a value of undefined. To update the variable value, we pass the forwardRef function, the result of which is a memory reference to the variable whose value is the class/function. Therefore, when that memory reference is updated with the class instance, the result of the forwardRef function is no longer undefined, but is the memory reference of the class/function.

You Probably Will Not Need It

While many tutorials and code samples, especially ones including NG_VALUE_ACCESSOR, make use of forwardRef, in most cases this is unnecessary. If we follow the Angular style guide recommendations, the single responsibility principle of file organization dictates having one file per component, service, directive, etc. This means the class to which we provide a forward reference will be in a separate file. This class gets defined before we need it thanks to the Angular Dependency Injector. Angular’s dependency injector creates a dependency tree of our files based on their import statements. Between the dependency tree and JavaScript module syntax, Angular is able to guarantee our files have access to the references they need.

Circular Dependencies

One case where we may violate the single-file-per-class recommendation is when we would otherwise create circular dependencies. The best way to explain this situation is probably to jump in to a code example. We will use the Tabs navigation bar of the the Angular Material library.

The _MatTabNavBase class of Angular Material contains a list of links (the _items property). These links are the tabs users click to display different content sections. _MatTabNavBase needs access to these links to monitor them for changes. When it detects the list of links has changed, the navigation menu is responsible for focusing and scrolling to the selected content and an “ink bar” visually indicates the link is active. So far, so good. We do not have a need for a forwardRef.

@Directive()
export abstract class _MatTabNavBase extends MatPaginatedTabHeader implements AfterContentChecked,
  AfterContentInit, OnDestroy {

  /** Query list of all tab links of the tab navigation. */
  abstract _items: QueryList<MatPaginatedTabHeaderItem & {active: boolean}>;

  // ... Code removed for brevity

  ngAfterContentInit() {
    // We need this to run before the `changes` subscription in parent to ensure that the
    // selectedIndex is up-to-date by the time the super class starts looking for it.
    this._items.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => {
      this.updateActiveLink();
    });

    super.ngAfterContentInit();
  }

  /** Notifies the component that the active link has been changed. */
  updateActiveLink() {
    if (!this._items) {
      return;
    }

    const items = this._items.toArray();

    for (let i = 0; i < items.length; i++) {
      if (items[i].active) {
        this.selectedIndex = i;
        this._changeDetectorRef.markForCheck();
        return;
      }
    }

    // The ink bar should hide itself if no items are active.
    this.selectedIndex = -1;
    this._inkBar.hide();
  }
}

Now consider the _MatTabLinkBase class. _MatTabLinkBase represents an individual link on the _MatTabNavBase navigation menu. When a user clicks the link, _MatTabLinkBase marks the link as active and calls _MatTabNavBase’s updateActiveLink method to update the selected index.

@Directive()
export class _MatTabLinkBase extends _MatTabLinkMixinBase implements AfterViewInit, OnDestroy,
  CanDisable, CanDisableRipple, HasTabIndex, RippleTarget, FocusableOption {

  /** Whether the tab link is active or not. */
  protected _isActive: boolean = false;

  /** Whether the link is active. */
  @Input()
  get active(): boolean { return this._isActive; }
  set active(value: boolean) {
    const newValue = coerceBooleanProperty(value);

    if (newValue !== this._isActive) {
      this._isActive = value;
      this._tabNavBar.updateActiveLink();
    }
  }

Because _MatTabLinkBase needs access to _MatTabNavBase and _MatTabNavBase needs access to _MatTabLinkBase, we have a circular reference. If we define these two classes in separate files, the Angular Dependency Injector will identify the circular reference and complain. To satisfy the dependency injector, the Angular Material team placed both classes in the same file.

Putting both classes in the same file fixes the dependency injection problem, but now we have a problem with undefined. Since the classes get compiled into variables which are then hoisted like any other JavaScript variable, the first class' reference to the second class will find the referenced class’ value is undefined. The undefined value is a primitive in JavaScript, and primitive values in JavaScript are passed by value. This means even though the variable will later get updated to a function, the value we got in the first class from this variable will remain undefined.

forwardRef to the rescue! forwardRef accepts a function. When the first class is set to our hoisted variable, the variable is inside the forwardRef function. By the time forwardRef function gets called at runtime, the browser will have interpreted our class definition and updated the variable’s value. All is well!

You can find the relevant code on line 157 of the MatTabNav class that extends _MatTabNavBase:

export class MatTabNav extends _MatTabNavBase {
  @ContentChildren(forwardRef(() => MatTabLink), {descendants: true}) _items: QueryList<MatTabLink>;

More Info

For a great video on forwardRef check out:

Mezhenskyi, D. (2021, Mar 30). ForwardRef Function in Angular (Advanced, 2021) [Video]. YouTube. https://www.youtube.com/watch?v=uKLvqohfp6I

Angular