Lazy‑Loaded Related Records in Ember Octane: Async Relationships + @tracked
When an Ember Octane app needs to fetch related data on demand, Ember Data’s async relationships combined with @tracked provide a clean, efficient pattern. This post walks through the problem, the solution, a worked example, trade‑offs, and actionable next steps.
18 Mar 2026, 05:33 UTC

Problem: Unnecessary Payloads and UI Blocking
Many Ember applications expose a list of items that each have nested data – for example, a blog post with comments. Loading all comments for every post in the initial payload is wasteful, especially on mobile or low‑bandwidth connections. At the same time, developers want the UI to remain responsive; a single large request can block rendering and cause a poor user experience.
Thesis: Combine Async Relationships with Tracked State for Lazy Loading
Ember Data’s async: true flag turns a relationship into a promise that is resolved only when the relationship is accessed. When this promise is stored in a @tracked property, the component re‑renders only after the data arrives, keeping the UI lean and responsive.
1. Defining an Async Relationship
In Ember Octane, models are defined with native classes. Add async: true to a hasMany or belongsTo declaration to enable lazy loading.
// app/models/post.js
import Model, { attr, hasMany } from '@ember-data/model';
export default class PostModel extends Model {
@attr title;
@attr('date') publishedAt;
@hasMany('comment', { async: true }) comments;
}
With this setup, post.get('comments') will return a PromiseObject that resolves to an array of Comment records when first accessed.
2. Tracking the Promise in a Component
Use the @tracked decorator to store the promise. The component only re‑renders when the promise resolves (or rejects).
// app/components/post-comments.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class PostCommentsComponent extends Component {
@tracked commentsPromise;
constructor() {
super(...arguments);
// Initially null – no network request yet
this.commentsPromise = null;
}
// Trigger lazy load when the user clicks "Show comments"
async loadComments() {
this.commentsPromise = this.args.post.get('comments');
try {
await this.commentsPromise;
} catch (e) {
this.error = e;
}
}
}
In the template, you can bind to this.commentsPromise directly. Because it is a PromiseObject, the template can display a loading state automatically.
{{!-- app/templates/components/post-comments.hbs --}}
Show comments
{{#if this.error}}
Failed to load comments: {{this.error.message}}
{{else if this.commentsPromise.isPending}}
Loading comments…
{{else if this.commentsPromise.isSettled}}
{{#each this.commentsPromise.content as |comment|}}
- {{comment.text}}
{{/each}}
{{/if}}
Key points:
- The network request is made only when
loadCommentsis called. - The UI shows a loading spinner while the promise is pending.
- When the promise resolves,
@trackedtriggers a re‑render, displaying the comments.
3. Verifying Lazy Loading
To confirm that the request is truly lazy:
- Open the application in a browser and navigate to a page that lists posts.
- Open the Network panel and clear existing entries.
- Click the "Show comments" button for a post. You should see a single
GET /api/posts/1/commentsrequest appear. - Refresh the page and ensure no
commentsrequests are made during the initial load.
These steps validate that the payload is reduced and the UI remains responsive.
4. Trade‑offs and Limitations
- Multiple Small Requests: If a user opens comments for many posts, the app will fire many small requests. In high‑traffic scenarios, consider batching or prefetching.
- Error Handling: A rejected promise silently disappears if you don’t expose an error state. Always catch errors and provide user feedback.
- Compatibility: Async relationships are supported only in Ember Data 3.28+. Older projects need to upgrade or use
belongsTo/hasManywithoutasync. - Prefetching Needs: When a relationship is accessed frequently, lazy loading may introduce latency on each access. In such cases, prefetch the relationship in the route or use a service to cache it.
Actionable Next Steps
- Audit your models: Identify relationships that are rarely needed on initial load and mark them as
async: true. - Wrap promises in
@tracked: In components where the relationship is accessed on demand, store thePromiseObjectin a tracked property. - Provide UI fallbacks: Use
isPending,isRejected, andisSettledflags to show loading spinners or error messages. - Measure the impact: Compare the initial payload size before and after enabling async relationships. Use the browser’s network panel or a tool like WebPageTest.
- Consider prefetching for hot paths: If a relationship is accessed in a critical user flow, load it in the route’s
model()hook or cache it in a service.
By following this pattern, you can keep your Ember Octane app lightweight, responsive, and maintainable while still providing a rich user experience.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.