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