Skip to content

Ext JS Routing and History — URL-Based Navigation and Browser History

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Ext JS Routing and History. We cover key concepts, practical examples, and best practices to help you master this topic.

Ext JS routing enables URL-based navigation where routes map to controller actions, supporting parameter Parsing, browser back/forward buttons, before-action guards, and deep-linking to specific application states.

What You'll Learn

  • Defining routes in controllers
  • Route parameters and query strings
  • Browser history integration
  • Before action hooks for guards
  • Route-to-view mapping patterns

Why It Matters

Users expect bookmarkable URLs and browser back/forward support in web applications. Ext JS routing maps URL patterns to controller actions, parses parameters, manages history, and enables deep-linking without page reloads.

Real-World Use

A project management app where #projects/123/tasks opens the specific project's task list directly, browser back returns to the project list, and the URL can be shared with teammates for direct access.

Routing Architecture

flowchart LR
    A[URL Hash] --> B[Router]
    B --> C[Route Handler]
    C --> D[Controller Action]
    D --> E[Update View]
    A --> F[History]
    F --> G[Back/Forward]
    D --> H[before Action]
    H --> I[Guard Check]
    I --> J[Allow]
    I --> K[Block]
    style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px

Basic Route Configuration

Ext.define('MyApp.controller.Routes', {
  extend: 'Ext.app.Controller',
  routes: {
    'dashboard': 'onDashboard',
    'users': 'onUsers',
    'users/:id': 'onUserDetail',
    'users/:id/edit': 'onUserEdit',
    'products/:category/:id': 'onProductDetail',
    'reports/:type?': 'onReports' // optional parameter
  },
  onDashboard: function() {
    this.showView('MyApp.view.Dashboard');
  },
  onUsers: function() {
    this.showView('MyApp.view.users.Grid');
  },
  onUserDetail: function(id) {
    var store = this.getUsersStore();
    store.load({
      params: { id: id },
      callback: function(records) {
        this.showView('MyApp.view.users.Detail', { record: records[0] });
      },
      scope: this
    });
  },
  onUserEdit: function(id) {
    this.showView('MyApp.view.users.Form', { userId: id });
  },
  onProductDetail: function(category, id) {
    console.log('Product:', category, id);
  },
  onReports: function(type) {
    type = type || 'default';
    this.showView('MyApp.view.reports.' + Ext.String.capitalize(type));
  },
  // Helper to update the main content region
  showView: function(viewClass, config) {
    var main = Ext.getCmp('main-content');
    main.removeAll(true);
    main.add(Ext.create(viewClass, config || {}));
  }
});

Expected output: Navigating to #users/42/edit calls onUserEdit with id=42. The controller creates the user form view and adds it to the main content panel.

Route with Query Parameters

Ext.define('MyApp.controller.Products', {
  extend: 'Ext.app.Controller',
  routes: {
    'products': {
      action: 'onProducts',
      before: 'beforeProducts'
    }
  },
  beforeProducts: function(action) {
    // Guard: check authentication
    if (!MyApp.user.isLoggedIn) {
      Ext.Msg.alert('Login Required', 'Please log in to view products.');
      action.stop();
    } else {
      action.resume();
    }
  },
  onProducts: function() {
    // Access query string parameters via Ext.History
    var params = Ext.Object.fromQueryString(window.location.hash.split('?')[1] || '');
    var store = this.getProductsStore();
    store.load({
      params: {
        category: params.category,
        page: params.page || 1,
        sort: params.sort || 'name'
      }
    });
    this.showView('MyApp.view.products.Grid');
  }
});

// URL example: #products?category=electronics&page=2&sort=price

Expected output: Navigating to #products?category=electronics loads the products grid filtered by category with pagination. Query parameters control store loading.

Before Action Guards

Ext.define('MyApp.controller.Orders', {
  extend: 'Ext.app.Controller',
  routes: {
    'orders/:id/edit': {
      action: 'onEditOrder',
      before: 'beforeEditOrder'
    },
    'orders/:id/delete': {
      action: 'onDeleteOrder',
      before: 'beforeDeleteOrder'
    }
  },
  beforeEditOrder: function(id, action) {
    var store = this.getOrdersStore();
    var record = store.getById(id);
    if (!record) {
      // Load record first, then resume
      store.load({
        params: { id: id },
        callback: function(records) {
          if (records && records.length) {
            action.resume();
          } else {
            action.stop();
            Ext.Msg.alert('Not Found', 'Order not found.');
          }
        }
      });
    } else if (record.get('status') === 'shipped') {
      action.stop();
      Ext.Msg.alert('Cannot Edit', 'Shipped orders cannot be edited.');
    } else {
      action.resume();
    }
  },
  beforeDeleteOrder: function(id, action) {
    Ext.Msg.confirm('Delete Order', 'Are you sure?', function(btn) {
      if (btn === 'yes') {
        action.resume();
      } else {
        action.stop();
      }
    });
  },
  onEditOrder: function(id) {
    this.showView('MyApp.view.orders.Form', { orderId: id });
  },
  onDeleteOrder: function(id) {
    var store = this.getOrdersStore();
    store.removeAt(store.find('id', parseInt(id)));
    store.sync();
    this.redirectTo('orders');
  }
});

Expected output: Before guards check permissions, load missing data, or show confirmations before the route action executes. If the guard stops, the route action never runs.

Programmatic Navigation

Ext.define('MyApp.controller.Navigation', {
  extend: 'Ext.app.Controller',
  routes: {
    '': 'onHome'
  },
  refs: [{
    ref: 'navTree',
    selector: 'navigationtree'
  }],
  init: function() {
    // Listen to tree item clicks for navigation
    this.control({
      'navigationtree': {
        itemclick: 'onNavItemClick'
      }
    });
  },
  onNavItemClick: function(tree, record) {
    var route = record.get('route');
    if (route) {
      // Navigate programmatically
      this.redirectTo(route);
    }
  },
  onHome: function() {
    this.showView('MyApp.view.Dashboard');
  },
  // Example: Navigate from controller
  showUser: function(userId) {
    this.redirectTo('users/' + userId);
  },
  // Navigate with query params
  showProducts: function(category, page) {
    this.redirectTo('products?category=' + category + '&page=' + page);
  }
});

Expected output: Clicking a tree node with a route property navigates via redirectTo, which updates the URL hash and triggers the matching route handler.

History Management

Ext.define('MyApp.controller.History', {
  extend: 'Ext.app.Controller',
  routes: {
    '*path': 'onUnknownRoute'
  },
  init: function() {
    // Listen to history changes
    Ext.History.on('change', function(token) {
      console.log('History changed to:', token);
    });
  },
  onUnknownRoute: function(path) {
    console.log('Unknown route:', path);
  },
  // Multi-step navigation (e.g., wizard)
  startWizard: function() {
    // Push state without triggering route
    this.redirectTo('wizard/step1');
  },
  nextStep: function(currentStep) {
    var next = 'wizard/step' + (currentStep + 1);
    // Replace current history entry (no back to step1)
    this.redirectTo(next, true); // true = replace
  }
});

// Browser support:
// Uses HTML5 History API if available, falls back to hashchange
// Enable with router: { type: 'history' } in Application config

Expected output: Browser back/forward buttons navigate through route history. The replace option avoids filling history with intermediate wizard steps.

Route Organization Patterns

// Pattern 1: Routes inline in controller
Ext.define('MyApp.controller.Users', {
  extend: 'Ext.app.Controller',
  routes: {
    'users': 'list',
    'users/create': 'create',
    'users/:id': 'show',
    'users/:id/edit': 'edit'
  }
});

// Pattern 2: Centralized router controller
Ext.define('MyApp.controller.Router', {
  extend: 'Ext.app.Controller',
  routes: {
    'users': { controller: 'users', action: 'list' },
    'users/:id': { controller: 'users', action: 'show' },
    'products': { controller: 'products', action: 'list' },
    'products/:id': { controller: 'products', action: 'show' }
  }
});

// Pattern 3: Application-level routes
Ext.define('MyApp.Application', {
  extend: 'Ext.app.Application',
  routes: {
    'dashboard': {
      viewController: 'MyApp.view.DashboardController',
      action: 'onDashboard'
    }
  }
});

Expected output: Choose the pattern based on project size. Inline routes suit small modules. Centralized routing helps large teams. Application routes work for cross-cutting concerns.

Common Mistakes

  1. Not calling action.resume() in before guards - If before guards never resume, the route action silently never executes. Always call resume() after async operations complete.

  2. Mixing hash and pushState routes - Choose one Strategy. Hash routes (#/users) work everywhere without server config. PushState routes (/users) require server-side fallback for direct access.

  3. Forgetting to decode URL parameters - URL-encoded characters (%20, %40) need decodeURIComponent(). Ext JS parses route parameters but raw query strings need manual decoding.

  4. Creating infinite redirect loops - A route handler that calls redirectTo to the same route causes a loop. Always check current state before redirecting.

  5. Not handling unknown routes - Add a catch-all route (*) to handle invalid URLs and show a 404 or redirect to default.

Practice Questions

  1. How do you define a route with an optional parameter?
  2. What is the purpose of the before action in route configuration?
  3. How do you navigate programmatically to a route?
  4. How does Ext.History integrate with browser back/forward buttons?
  5. What happens if a before guard calls action.stop()?

Challenge: Build a routed application with: a sidebar navigation tree, routes for users (list, detail, edit), products (list with category filter via query params), orders (with before guard checking edit permission), a default dashboard route, a 404 catch-all, and programmatic navigation from grid row clicks.

FAQ

What is the difference between redirectTo and location.hash?

redirectTo('users/42') updates Ext.History and triggers the route handler. Setting location.hash directly updates the URL but does not trigger Ext JS route matching.

Can I use HTML5 pushState instead of hash-based URLs?

Yes. Set router: { type: 'history' } in your Application config. This uses pushState for clean URLs but requires server-side URL rewriting.

How do I pass multiple parameters in a route?

Use route 'users/:userId/orders/:orderId' and the handler receives both parameters: onUserOrder(userId, orderId). For complex data, use query strings.

Can I lazy-load controllers when a route is matched?

Yes. Declare the controller in the route config: { controller: 'users', action: 'list' }. The controller loads on first route match.

How do I prevent navigating away from unsaved changes?

Use a before guard on all routes that checks a global dirty flag. If dirty, show a confirm dialog and conditionally stop or resume the route.

Mini Project

Build a full routed application with: a main viewport with sidebar (tree) and content area (card layout), routes for Dashboard, Users (list/detail/edit), Products (list with category filter, detail), Settings (with tabs), a before guard on all edit routes that checks for unsaved changes, programmatic navigation from grid row double-click, a catch-all 404 route, and browser back/forward support.

What's Next

Routing handles navigation. Learn how Ext JS Theming and Styling customizes the look and feel of your application with themes, CSS overrides, and Sencha Themer.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro