Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Evagoras Charalambous
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Evagoras Charalambous

Router's loadChildren Callback Doesn't Have To Be A Fat-Arrow In Angular 14

By
Published in

With the release of standalone components / optional modules in Angular 14, I've been trying to dip my toe back in the Angular pool. But, as I was reading up on the Router's loadChildren property for lazy-loading routes, something in the documentation struck me as odd. It states that the loadChildren property needs to use the Fat-arrow syntax:

LoadChildrenCallback (type alias): A function that is called to resolve a collection of lazy-loaded routes. Must be an arrow function of the following form:

() => import('...').then(mod => mod.MODULE)
or
() => import('...').then(mod => mod.ROUTES)

The reason this seemed like a strange requirement is because a Fat-arrow function is just a function. It doesn't actually do anything different than a traditional function declaration / expression other then retain the this binding; and, depending on how it's authored, can create an implied return statement. But, those facets are up to the discretion of the developer writing the code - it should have absolutely no bearing at all on how that code is being consumed.

To demonstrate, let's define a Route[] collection that does not use the Fat-arrow syntax when lazy-loading a sub-tree of the router configuration. In the following routes file, the /admin path is going to be lazy-loaded:

// Import core Angular modules.
import { Route } from "@angular/router";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

/**
* NOTE ABOUT IMPORT(): While the native import() function is capable of loading a dynamic
* path, all import paths within our Angular application must be static. This is because
* Webpack performs a static analysis of the import() calls at BUILD TIME and therefore
* cannot consume paths that are defined at RUNTIME. The Angular documentation also states
* that the loadChilren() call must use a FAT-ARROW function; however, this is not true.
* The loadChildren() call can use a normal Function as long as it returns the result of a
* static import() call. I suspect that the Fat-Arrow constraint in the documentation is
* more about making the line of code LOOK as if it were an inline call instead of deferred
* call.
* 
* See GitHub: https://github.com/webpack/webpack/issues/6680
*/
export var ROUTES: Route[] = [
	{
		path: "admin",
		loadChildren: function lazy() {

			return( getRoutes( import( "./admin-view/admin-view.routes" ) ) );

		}
	}
];

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

/**
* I provide a method that simplifies the lazy-loading of routes from the given path.
*/
export async function getRoutes(
	routesPromise: Promise<any>,
	routesKey: string = "ROUTES"
	 ) : Promise<Route[]> {

	var mod = await routesPromise;

	return( mod[ routesKey ] );

}

As you can see, there's no Fat-arrow function anywhere in this file. There's simply a Callback being defined using a "traditional" function expression. Inside that callback, I'm then invoking the native import() function to load the Admin's routes file and I'm using the getRoutes() helper function to "unwrap" the resultant Promise.

And, when we run this Angular 14 code and navigate to the /admin route, we can see the routes file get lazy-loaded in the network:

Network activity showing that the admin routes file was loaded on-the-fly when navigating to the admin URL in Angular 14

Of course, in some cases, the Fat-arrow function can be used for nothing more than to reduce the amount of code on the screen. The above code can be rewritten to be slightly more concise (the overall code in this snippet is being truncated to highlight differences):

export var ROUTES: Route[] = [
	{
		path: "admin",
		loadChildren: () => getRoutes( import( "./admin-view/admin-view.routes" ) )
	}
];

This is the same exact thing. Only, I'm using the Fat-arrow syntax as a convenience in order to remove the Function's body-brackets and explicit return statement. And, I'm still using the getRoutes() helper function as a means to hide the Promise handling / unwrapping.

Normally, I am extremely turned-off by "one liner" Fat-arrow functions. I find them hard to read and hard to debug since there's very little "wiggle room" for additional expressions. But, in this case, I tend to agree that it makes the code a bit more readable since it removes much of boiler-plate syntax and makes the necessarily deferred import() invocation feel more collocated with the route.

Webpack Performs a Static Analysis of import() Calls at Build Time

Once you define a few routes this way, it's natural to see a pattern emerge. And, it's natural to want to further clean things up by creating a Factory function that takes the file path and generates the loadChildren callback. Something akin to:

export var ROUTES: Route[] = [
	{
		path: "admin",
		// !!!!!!!!!! CAUTION: THIS DOES NOT WORK !!!!!!!!!! //
		// !!!!!!!!!! CAUTION: THIS DOES NOT WORK !!!!!!!!!! //
		loadChildren: lazyLoad( "./admin-view/admin-view.routes" )
		// !!!!!!!!!! CAUTION: THIS DOES NOT WORK !!!!!!!!!! //
		// !!!!!!!!!! CAUTION: THIS DOES NOT WORK !!!!!!!!!! //
	}
];

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

function lazyLoad( routesPath: string ) : LoadChildrenCallback {

	return function() {

		return( import( routesPath ).then( m => m.ROUTES ) );

	};

}

To me, this code feels even more readable. And, it even compiles! However, unfortunately, it doesn't work. When you try to navigate to the /admin route, you'll see this error in the developer console:

ERROR Error: Uncaught (in promise):
Error: Cannot find module './admin-view/admin-view.routes'

The reason for this, as best I understand it, is that Angular is using Webpack under the hood for all the bundling and code-splitting. And, according to this GitHub issue, Webpack performs a static analysis of import() statements at build time in order to bundle and split-up the code. As such, it cannot work with dynamic paths that are evaluated at run time (such as we have in our previous code snippet).

In the end, I'll almost certainly use the Fat-arrow syntax in some way when defining my lazy-loaded routes in Angular 14 because it reduces some of the noise without making it too magical. However, it should be clear to Angular developers that this is a stylistic choice only and has nothing to do with how the Angular code operates.

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel