Exploring Angular 18’s RedirectCommand class and @let block

leemeganj

Megan Lee

Posted on June 26, 2024

Exploring Angular 18’s RedirectCommand class and @let block

Written by Lewis Cianci✏️

In a single day, 385,000 babies are born, the sun evaporates over a trillion tons of water off the surface of the ocean, and fifteen new stable versions of Angular are released. Okay, that’s an exaggeration (not the babies and evaporation bit!), but the Angular release cycle these days is certainly quick — and it’s not slowing down.

This latest update brings a couple of notable quality-of-life improvements. First of all, redirects within Angular can now occur by way of a RedirectCommand, which allows greater control over how a redirect occurs and easier implementation of best practices. Secondly, we can now declare variables within the view themselves, which can be helpful.

How do we use these features? Let’s take a look.

Let me redirect you: The RedirectCommand class

Why is redirecting such a headline feature in Angular 18? We’re literally just shifting from one page to another. But navigation — something that we take for granted when we click on a link — is deceptively simple.

Consider a simple website with some trivial routes:

  • / — The root page of the application
  • /admin — Where administrators can see everyone's journal entries
  • /write — Where users write journal entries

Everyone should be able to access the application root, including anonymous users. But should everyone be able to access the admin route? Of course not — that’s just a cybersecurity risk waiting to happen.

What about if users are on the write route, and they try to navigate to the root route? That’s fine from a security perspective, but they might lose what they’re writing! Ideally, we’d want to show a confirmation dialog before performing that routing operation.

Within the Angular nomenclature, these actions essentially boil down to the following actions:

  • canMatch: Sometimes, more than one route can match a given path. If a user is an admin, they should see the admin write route. Otherwise, they should see the normal write page
  • canActivate: If a route is allowed to be activated or not. For example, the write route should only be accessible if the user is logged in
  • canDeactivate: If a user can navigate away from a route. Typically used to show a dialog box when navigating away from a page with changed data

To help understand the power of RedirectCommand, let’s create a sample application called Journalpedia that has some of this basic routing functionality. Journalpedia will look very plain, and that’s okay, because the focus of this article is on routing and not visual appeal.

For the purposes of this article, we’ll have our app.component.html contain a router-outlet, but it will also contain a Login Simulator that lets us change between user types on the fly. The idea is that we can route to anywhere on the test site and still be able to change between accounts:

<router-outlet></router-outlet>
<div style="bottom: 0; right: 0; height: 200px; width: 200px; background-color: wheat; position: fixed">
  <app-login-simulator></app-login-simulator>
</div>
Enter fullscreen mode Exit fullscreen mode

The front page of our app will look like this: Homepage For Demo Angular App Showing Welcome Message, Three Buttons, And Login Simulator Our app has three buttons. Typically, we wouldn’t display the Admin or Write a journal entry buttons because we don’t want users to try to click on them before logging in, but in this article we’ll show them anyway so we can see how route guards and activation work.

Normally, users could log in or change their logins at any time, but we don’t have a fully fledged authentication system in place. Instead, we can click on our Login Simulator to simulate a user logging in.

Why do we need the RedirectCommand class?

To clarify, redirect functionality has long been part of Angular’s feature set. So why did Angular 18 introduce an entirely new class? Let’s see how redirection occurs in Angular apps today to see why RedirectCommand is an improvement.

Typically, routes within an application like this would look like the following:

export const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
  },
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [() => {
      const router = inject(Router);
      const auth = inject(AuthenticationService);
      if (auth.userLogin$.value == LoginType.AdminUser){
        return true;
      }
      else{
        router.navigate(['/unauthorized']);
        return false;
      }
    }],
  },
  {
    path: 'write',
    component: WriteComponent,
    canActivate: [() => {
      const router = inject(Router);
      const auth = inject(AuthenticationService);
      if (auth.userLogin$.value != undefined) {
        return true;
      } else {
        router.navigate(['/unauthorized']);
        return false;
      }
    }]
  },
  {
    path: 'unauthorized',
    component: UnauthorizedComponent
  }
];
Enter fullscreen mode Exit fullscreen mode

The crux of the routing operation here is to check that a user is of the correct type, and confirm that the navigation can happen. Otherwise, redirect to the unauthorized page.

For most applications, this is fine, but it introduces a dependency on returning either a true or false value from the guard. If we’re returning true, the navigation will complete, but if it returns false, then the navigation will just not occur. Sometimes, this can cause developers and consumers alike to become confused at the lack of routing occurring.

Fortunately, we can return a URL tree directly from within our canActivate function, specifying a direct path to another page, like so:

{
  path: 'admin',
  component: AdminComponent,
  canActivate: [() => {
    const router = inject(Router);
    const auth = inject(AuthenticationService);

    if (auth.userLogin$.value == LoginType.AdminUser){
      return true;
    }
    return router.parseUrl('/unauthorized')
  }],
}
Enter fullscreen mode Exit fullscreen mode

This is a better pattern for two reasons: first, we’re not flat-out rejecting the navigation. Second, something will always occur — either the navigation will succeed, or it will return an alternative route.

The only wrinkle in this approach is that we can’t pass extra NavigationExtra properties in parseUrl.

NavigationExtra properties pave the way for a bunch of advanced routing scenarios — for example, how query parameters should be handled, whether a location change should be skipped or not. They also give us the opportunity to pass data to the navigated page via the state property.

Fortunately, that’s exactly where RedirectCommand comes in. RedirectCommand accepts a UrlTree, but also accepts NavigationExtra properties. We can redirect to another page and use NavigationExtra properties at the same time. Because it’s a new class, it doesn’t break any existing functionality, which is great!

So, our admin route becomes:

{
  path: 'admin',
  component: AdminComponent,
  canActivate: [() => {
    const router = inject(Router);
    const auth = inject(AuthenticationService);

    if (auth.userLogin$.value == LoginType.AdminUser) {
      return true;
    }
    return new RedirectCommand(router.parseUrl('/unauthorized'), {
      skipLocationChange: true, state: {
        loginDuringAuth: auth.userLogin$.value
      }
    })
  }],
},
Enter fullscreen mode Exit fullscreen mode

With this, we don’t return false from the canActivate function, and we can take full advantage of the NavigationExtra superpowers. In this example, we observe something that happened during the routing operation and put that in the state variable.

Within the constructor for the unauthorized page, we can retrieve this state variable, like so:

constructor(private _location: Location, private _router: Router) {
  console.log(this._router.getCurrentNavigation()?.extras.state);
  this.loginDuringAuth.set(_router.getCurrentNavigation()?.extras?.state?.['loginDuringAuth']);
}
Enter fullscreen mode Exit fullscreen mode

Then, we can make the changes to our unauthorized page:

<h2>Unauthorized :(</h2>

@if (this.loginDuringAuth() == undefined){
  The router thinks you're not logged in.
} @else {
  The router thinks you're logged in as {{LoginType[this.loginDuringAuth()!]}}
}
<br>
<button (click)="goBack()">Go back</button>
Enter fullscreen mode Exit fullscreen mode

Now, we pass through the router state and display the result on the screen: Example Admin Route For Demo Angular App Blocked For Normal User With Message Explaining Why Route Is Blocked And Providing A Button To Go Back Having RedirectCommand provides a favorable outcome because all existing routing operations will still work and more complex routing operations are possible.

Let it be: The @let operator

The other notable addition to Angular 18 — specifically, the incremental v18.1 release — is the @let operator. As Angular developers, we’re likely already familiar with the let operator in TypeScript, along with var, const, etc. But now it’s in the template code, so what gives?

Essentially, @let lets us define and assign variables within the template itself. If that sounds scary to you, that’s totally understandable. It’s not so hard to imagine how people would completely misuse this and declare way-too-complex business logic in their views. In the long run, that’s going to hurt debugging and troubleshooting.

Still, @let is a good addition because it means that we can easily access asynchronous variables with the async pipe without worrying about being personally responsible for unsubscribing. How would our unauthorized page change if we used @let?.

Instead of using @if statements, we can update our code like this:

<h2>Unauthorized :(</h2>

@let authStatus = this.loginDuringAuth() ?? 'Not logged in';

The router thinks you're logged in as {{authStatus}}

<button (click)="goBack()">Go back</button>
Enter fullscreen mode Exit fullscreen mode

That’s a pretty basic example, but if you have a single view and you just want to quickly declare a variable in the view, it becomes quite simple.

When not to use the @let operator in Angular

There are many ways to use the @let keyword. But let’s pause for some real talk — as developers, we’ve all encountered code that is badly written or isn’t easy to follow. If you use @let to define all of your application logic in your views, and forego writing any more TypeScript, you’re likely to gain an unfavorable reputation.

Imagine the horror! You’ll notice that your co-workers stop talking when you enter the room. Nicknames like “Letty” will be assigned. Tales will be told about Letty, the developer who just used @let everywhere and created an un-debuggable mess.

Fabricated drama aside, if you find yourself starting to use @let to complete complicated business logic, or copy-and-pasting it over and over again in your code, stop. You’re on your way to creating an unintelligible mess and the people you work with will be upset with your life choices.

@let exists in the view (or the template) to provide view-related functionality. So, use it for that — and don’t overuse it.

Using @let isn’t a magic bullet for performance either, and completing computationally intensive operations within a @let block will cause browser lockups and other bad things to happen. They should be completed within TypeScript — but you already knew that, right?

Feels good to be an Angular developer

We’re definitely getting a lot of DX enhancements in Angular, and I for one am enjoying it immensely. These changes will continue to enhance Angular’s ease of use when developing your apps for the web.

Feel free to clone the project and see for yourself how these changes make a difference.


Experience your Angular apps exactly how a user does

Debugging Angular applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Angular state and actions for all of your users in production, try LogRocket.

LogRocket Signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site including network requests, JavaScript errors, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket NgRx plugin logs Angular state and actions to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Angular apps — start monitoring for free.

💖 💪 🙅 🚩
leemeganj
Megan Lee

Posted on June 26, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related