Loading Substates

A Template That Renders While `model()` Awaits

Loading Substates

A loading.hbs template renders during slow transitions — global or per-route — so users see a spinner or skeleton instead of a stale page.

4 min read Level 2/5 #ember#routing#loading
What you'll learn
  • Add a top-level app/templates/loading.hbs
  • Add per-route loading templates
  • Combine loading state with skeleton UIs

When a route’s model() returns a Promise, the user is waiting. Ember auto-renders a loading substate for you if one exists — convention beats wiring.

Global Loading

{{! app/templates/loading.hbs }}
<div class="app-loader">
  <Spinner />
  <p>Loading…</p>
</div>

This renders during any transition where the parent’s model is still pending.

Per-Route Loading

For the posts route, create app/templates/posts/loading.hbs:

{{! app/templates/posts/loading.hbs }}
<div class="posts-skeleton">
  {{#each (array 1 2 3 4 5) as |n|}}
    <div class="skeleton-row" />
  {{/each}}
</div>

Per-route templates win over the global one when both exist.

Loading Route Hook

You can also implement loading as a route action:

// app/routes/posts.js
import Route from '@ember/routing/route';
import { action } from '@ember/object';

export default class PostsRoute extends Route {
  @action
  loading(transition) {
    // Returning true (or letting it bubble) renders the loading substate
    // Returning false short-circuits, hiding the substate
    return true;
  }
}

Return false if you want a “stay on the previous page until loaded” experience.

When Substates Trigger

Ember only renders a loading substate if model(), beforeModel(), or afterModel() returns a Promise that is still pending. Synchronous returns skip it entirely.

Error Substates →