Skip to content

Ember Route Hooks — Data Loading and Lifecycle Methods

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Ember Route Hooks. We cover key concepts, practical examples, and best practices to help you master this topic.

Ember Route hooks are lifecycle methods that control data loading, authorization, redirects, and template setup. The model hook loads data for the template. beforeModel handles preconditions. afterModel processes loaded data. setupController wires data to the controller.

What You'll Learn

You will learn every route hook, when to use each one, how to handle loading and error states, and how to compose async operations in route hooks.

Why It Matters

Route hooks determine when and how data reaches the template. Proper use of hooks means fast page loads, clear error handling, and maintainable data loading logic.

Real-World Use

A dashboard application uses beforeModel to check authentication, model to load dashboard data, and setupController to prepare computed properties before the template renders.

flowchart LR
    A[Navigate to route] --> B[beforeModel]
    B --> C{Auth check}
    C -->|Fail| D[Redirect to login]
    C -->|Pass| E[model hook]
    E --> F[Fetch data]
    F --> G[afterModel]
    G --> H[setupController]
    H --> I[Render template]

The model Hook

The model hook fetches data and returns it. The template accesses the return value as this.model.

// app/routes/dashboard.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class DashboardRoute extends Route {
  @service store;

  async model() {
    // Fetch data from Ember Data
    let posts = await this.store.findAll('post');
    let users = await this.store.findAll('user');
    let stats = {
      totalPosts: posts.length,
      totalUsers: users.length,
      recentPosts: posts.slice(0, 5)
    };

    return stats;
  }
}
{{! app/templates/dashboard.hbs }}
<h1>Dashboard</h1>
<p>Total posts: {{this.model.totalPosts}}</p>
<p>Total users: {{this.model.totalUsers}}</p>

<h2>Recent Posts</h2>
<ul>
  {{#each this.model.recentPosts as |post|}}
    <li>{{post.title}}</li>
  {{/each}}
</ul>

The beforeModel Hook

Use beforeModel for preconditions like authentication checks.

// app/routes/admin.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class AdminRoute extends Route {
  @service session;

  beforeModel(transition) {
    if (!this.session.isAuthenticated) {
      console.warn('Access denied — redirecting to login');
      this.router.transitionTo('login');
    } else if (!this.session.user.isAdmin) {
      console.warn('Non-admin access denied');
      this.router.transitionTo('index');
    }
  }

  async model() {
    return this.store.findAll('admin-data');
  }
}

The afterModel Hook

Use afterModel to Process data after it is loaded but before the template renders.

// app/routes/reports.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class ReportsRoute extends Route {
  @service store;

  async model() {
    return this.store.findAll('report');
  }

  afterModel(reports, transition) {
    if (reports.length === 0) {
      console.log('No reports — redirecting to create page');
      this.router.transitionTo('reports.create');
      return;
    }

    // Attach computed data to the model
    reports.forEach(report => {
      report.analysisDate = report.get('createdAt').toLocaleDateString();
    });
  }
}

The setupController Hook

Use setupController to set additional properties on the controller beyond model.

// app/routes/search.js
import Route from '@ember/routing/route';

export default class SearchRoute extends Route {
  model(params) {
    return this.store.query('post', { q: params.query });
  }

  setupController(controller, model) {
    super.setupController(controller, model);
    controller.set('query', this.paramsFor('search').query);
    controller.set('hasResults', model.length > 0);
    controller.set('resultCount', model.length);
  }
}
// app/controllers/search.js
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';

export default class SearchController extends Controller {
  @tracked query = '';
  @tracked hasResults = false;
  @tracked resultCount = 0;

  get summary() {
    return `Found ${this.resultCount} results for "${this.query}"`;
  }
}

The loading Hook

Ember automatically shows a loading template while async operations are pending.

{{! app/templates/dashboard/loading.hbs }}
<div class="loading-spinner">
  <p>Loading dashboard data...</p>
</div>

You can customize loading behavior:

// app/routes/dashboard.js
import Route from '@ember/routing/route';

export default class DashboardRoute extends Route {
  @service store;

  model() {
    return this.store.findAll('post');
  }

  // Custom loading behavior
  loading(transition, originRoute) {
    // Show custom loading state
    console.log('Loading started');

    // Return true to use default loading template
    return true;
  }
}

The error Hook

Handle errors from route hooks:

// app/routes/dashboard.js
import Route from '@ember/routing/route';

export default class DashboardRoute extends Route {
  @service store;

  async model() {
    let response = await this.store.findAll('post');
    return response;
  }

  error(error, transition) {
    if (error.status === 404) {
      this.router.transitionTo('not-found');
    } else if (error.status === 500) {
      console.error('Server error:', error);
      this.router.transitionTo('error', { error: error });
    } else {
      // Let parent route handle it
      return true;
    }
  }
}

Resetting Route State

Use resetController when leaving a route to clean up state.

// app/routes/search.js
import Route from '@ember/routing/route';

export default class SearchRoute extends Route {
  resetController(controller, isExiting, transition) {
    if (isExiting) {
      controller.set('query', '');
      controller.set('results', []);
    }
  }
}

Common Mistakes

  1. Throwing errors instead of handling them. Always wrap async operations in try-catch and handle errors in the error hook.
  2. Not calling super.setupController(). Overriding setupController without calling super breaks the model assignment.
  3. Loading data in beforeModel. beforeModel is for preconditions. Load data in model hook where error handling is cleaner.
  4. Forgetting that model can return a promise. Ember waits for the promise. Use async/await for readability.
  5. Setting controller properties directly in the model hook. Use setupController instead. It is the designated place for controller setup.

Practice Questions

  1. What is the purpose of the model hook?
  2. When should you use beforeModel vs afterModel?
  3. How do you customize loading behavior in a route?
  4. What does setupController do?
  5. Challenge: Create a route that requires authentication (redirect to login if not authenticated), loads user dashboard data, and sets up a controller with user statistics. Handle 401 and 500 errors with different redirects.

FAQ

Can I have multiple model hooks in nested routes?

Yes. Each route has its own model hook. Nested route models can access parent route models.

Does the model hook run on every transition?

Yes, every time the route is entered. Use caching in the store to avoid redundant fetches.

What happens if model returns null?

The template receives null. Handle it with conditional rendering.

Can I use query params in the model hook?

Yes. Access them via params.queryParamName.

How do I abort a route transition?

Call transition.abort() in beforeModel.

Mini Project

Create a route with all hooks: beforeModel checks auth, model loads user profile and recent orders, afterModel computes order totals, setupController sets query params, loading shows a spinner, error handles 403 and 404. Test each hook by adding console.log statements.

What's Next

Now that you understand route hooks, learn Ember Nested Routes for complex layouts. Then explore Ember Templates for rendering data.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro