Skip to content

Aurelia Router Lifecycle — Navigation Hooks and Guards

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Aurelia Router Lifecycle. We cover key concepts, practical examples, and best practices to help you master this topic.

Aurelia's router lifecycle provides hooks for controlling navigation. The canActivate hook decides if navigation proceeds. activate loads data. canDeactivate confirms before leaving. deactivate cleans up. Pipeline steps intercept navigation globally.

What You'll Learn

You will learn every router lifecycle hook, how to implement route guards, handle unsaved changes, control navigation flow, and use pipeline steps for cross-cutting concerns.

Why It Matters

Route lifecycle hooks are essential for data loading, authentication, dirty form detection, and analytics tracking. Without them, pages load incomplete data or users lose unsaved work.

Real-World Use

A CRM application uses canActivate to check permissions, activate to load customer data, canDeactivate to warn about unsaved changes, and pipeline steps to track page views in analytics.

flowchart LR
    A[Navigation Start] --> B[canDeactivate (current)]
    B --> C{Confirmed?}
    C -->|No| D[Navigation cancelled]
    C -->|Yes| E[canActivate (next)]
    E --> F{Allowed?}
    F -->|No| G[Redirect]
    F -->|Yes| H[activate (next)]
    H --> I[Component rendered]
    I --> J[deactivate (current)]

The activate Hook

activate is called when the route is navigated to. It receives route parameters and the route configuration.

export class ProductDetail {
  activate(params, routeConfig, navigationInstruction) {
    console.log('Product ID:', params.id);
    console.log('Route config:', routeConfig.name);

    // Return a promise to wait for data loading
    return this.api.getProduct(params.id).then(product => {
      this.product = product;
    });
  }
}

The canActivate Hook

canActivate determines if navigation is allowed. Return true, false, a Redirect, or a promise resolving to one of these.

import { Redirect } from 'aurelia-router';
import { inject } from 'aurelia-framework';

@inject(AuthService)
export class AdminDashboard {
  constructor(auth) {
    this.auth = auth;
  }

  canActivate(params, routeConfig, navigationInstruction) {
    // Synchronous check
    if (!this.auth.isAuthenticated) {
      return new Redirect('login');
    }

    // Async check
    return this.auth.checkPermission('admin').then(allowed => {
      if (!allowed) {
        return new Redirect('forbidden');
      }
      return true;
    });
  }

  activate() {
    return this.api.getDashboardData();
  }
}

The canDeactivate Hook

canDeactivate is called before leaving a route. Use it to confirm unsaved changes.

export class SettingsForm {
  constructor() {
    this.hasUnsavedChanges = false;
  }

  canDeactivate() {
    if (this.hasUnsavedChanges) {
      return confirm('You have unsaved changes. Are you sure you want to leave?');
    }
    return true;
  }

  onFieldChange() {
    this.hasUnsavedChanges = true;
  }

  async save() {
    await this.api.save(this.formData);
    this.hasUnsavedChanges = false;
    this.router.navigateToRoute('settings');
  }
}

The deactivate Hook

deactivate runs after the route is navigated away from. Use it for cleanup.

export class LiveDashboard {
  constructor() {
    this.interval = null;
  }

  activate() {
    // Start live updates
    this.interval = setInterval(() => this.refresh(), 5000);
    return this.api.getDashboardData();
  }

  deactivate() {
    // Clean up interval
    if (this.interval) {
      clearInterval(this.interval);
      this.interval = null;
    }

    // Unsubscribe from event aggregator
    this.eventAggregator.unsubscribe(this.subscription);

    // Dispose of resources
    this.chart?.destroy();
  }
}

Pipeline Steps

Pipeline steps intercept every navigation globally. They are useful for auth, logging, and analytics.

// src/steps/auth-step.ts
import { Redirect } from 'aurelia-router';
import { inject } from 'aurelia-framework';

@inject(AuthService)
export class AuthStep {
  constructor(auth) {
    this.auth = auth;
  }

  run(navigationInstruction, next) {
    // Check if any route in the navigation requires auth
    const requiresAuth = navigationInstruction.getAllInstructions()
      .some(i => i.config.settings && i.config.settings.auth !== false);

    if (requiresAuth && !this.auth.isAuthenticated) {
      return next.cancel(new Redirect('login'));
    }

    return next();
  }
}
// src/steps/analytics-step.ts
export class AnalyticsStep {
  run(navigationInstruction, next) {
    const routeName = navigationInstruction.config.name || 'unknown';
    const url = navigationInstruction.fragment;

    // Track page view
    analytics.trackPageView(routeName, url);

    return next();
  }
}

Register steps:

configureRouter(config, router) {
  config.addPipelineStep('authorize', AuthStep);
  config.addPipelineStep('post-activate', AnalyticsStep);
  config.map([...]);
}

Complete Lifecycle Example

export class DocumentEditor {
  constructor(api, router, dialog) {
    this.api = api;
    this.router = router;
    this.dialog = dialog;
  }

  // 1. Can we navigate here?
  canActivate(params) {
    return this.api.checkAccess(params.id).then(allowed => {
      if (!allowed) throw new Error('Access denied');
      return true;
    });
  }

  // 2. Load data
  activate(params) {
    return this.api.getDocument(params.id).then(doc => {
      this.document = doc;
      this.originalContent = doc.content;
    });
  }

  // 3. Can we leave?
  canDeactivate() {
    if (this.document.content !== this.originalContent) {
      return this.dialog.confirm({
        title: 'Unsaved Changes',
        message: 'You have unsaved changes. Leave anyway?'
      });
    }
    return true;
  }

  // 4. Cleanup
  deactivate() {
    this.document = null;
    this.originalContent = null;
  }
}

Loading State During Navigation

<template>
  <!-- Show during route activation with loading screen -->
  <router-view></router-view>

  <!-- Or show a loading bar during all navigations -->
  <loading-indicator show.bind="router.isNavigating"></loading-indicator>
</template>
export class App {
  configureRouter(config, router) {
    this.router = router;
    // router.isNavigating becomes true during route transitions
  }
}

Error Handling in Lifecycle

export class DataRoute {
  canActivate() {
    return true;
  }

  activate(params) {
    return this.api.getData(params.id).catch(error => {
      this.error = error;
      // Do not throw — component renders with error state
    });
  }
}

Common Mistakes

  1. Throwing errors in canActivate. Throw redirects target. Use return new Redirect() for navigation control.
  2. Forgetting to return the promise from activate. Without returning the promise, the route activates before data is loaded.
  3. Not implementing canDeactivate for forms. Users lose unsaved changes without confirmation. Always guard dirty forms.
  4. Heavy work in canActivate. This hook blocks navigation. Keep it fast. Defer heavy data loading to activate.
  5. Not cleaning up in deactivate. Intervals, subscriptions, and listeners accumulate. Clean them up to prevent memory leaks.

Practice Questions

  1. What is the difference between canActivate and activate?
  2. How do you prevent a user from leaving a page with unsaved changes?
  3. How do you redirect unauthorized users to the login page?
  4. What are pipeline steps and when should you use them?
  5. Challenge: Create a multi-step checkout wizard with 4 steps. Each step validates before advancing. canDeactivate warns if the user tries to go back after completing a step. A pipeline step tracks the funnel in analytics. activate loads available shipping methods for step 2 and payment methods for step 3.

FAQ

What does `canActivate` return?

Return true to proceed, false to cancel, a Redirect object, or a promise that resolves to any of these.

Can I access the next route in `canDeactivate`?

Yes. canDeactivate receives the navigation instruction for the target route.

What is the order of lifecycle hooks?

canDeactivate (old) → canActivate (new) → activate (new) → deactivate (old)

How do I pass data between lifecycle hooks?

Store data on this in the component instance.

Can I cancel navigation after `activate`?

No. Once activate runs, navigation is committed. Use canActivate for guard logic.

Mini Project

Build a document editor with full lifecycle management: (1) canActivate checks read/write permission. (2) activate loads the document. (3) canDeactivate warns if there are unsaved changes. (4) deactivate auto-saves and releases locks. (5) Pipeline step logs all navigation to a history service. (6) Loading states show during activation.

What's Next

Now that you understand the router lifecycle, learn Aurelia Child Routers for nested navigation. Then explore Aurelia HTTP Client for server communication.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro