Files
marco/.agents/skills/ember-best-practices/rules/template-conditional-rendering.md

281 lines
6.6 KiB
Markdown

---
title: Optimize Conditional Rendering
impact: HIGH
impactDescription: Reduces unnecessary rerenders in dynamic template branches
tags: templates, conditionals, rendering, performance, glimmer
---
## Optimize Conditional Rendering
Use efficient conditional rendering patterns to minimize unnecessary DOM updates and improve rendering performance.
## Problem
Inefficient conditional logic causes excessive re-renders, creates complex template code, and can lead to poor performance in lists and dynamic UIs.
**Incorrect:**
```glimmer-js
// app/components/user-list.gjs
import Component from '@glimmer/component';
class UserList extends Component {
<template>
{{#each @users as |user|}}
<div class="user">
{{! Recomputes every time}}
{{#if (eq user.role "admin")}}
<span class="badge admin">{{user.name}} (Admin)</span>
{{/if}}
{{#if (eq user.role "moderator")}}
<span class="badge mod">{{user.name}} (Mod)</span>
{{/if}}
{{#if (eq user.role "user")}}
<span>{{user.name}}</span>
{{/if}}
</div>
{{/each}}
</template>
}
```
## Solution
Use `{{#if}}` / `{{#else if}}` / `{{#else}}` chains and extract computed logic to getters for better performance and readability.
**Correct:**
```glimmer-js
// app/components/user-list.gjs
import Component from '@glimmer/component';
class UserList extends Component {
<template>
{{#each @users as |user|}}
<div class="user">
{{#if (eq user.role "admin")}}
<span class="badge admin">{{user.name}} (Admin)</span>
{{else if (eq user.role "moderator")}}
<span class="badge mod">{{user.name}} (Mod)</span>
{{else}}
<span>{{user.name}}</span>
{{/if}}
</div>
{{/each}}
</template>
}
```
## Extracted Logic Pattern
For complex conditions, use getters:
```glimmer-js
// app/components/user-card.gjs
import Component from '@glimmer/component';
import { cached } from '@glimmer/tracking';
class UserCard extends Component {
@cached
get isActive() {
return this.args.user.status === 'active' && this.args.user.lastLoginDays < 30;
}
@cached
get showActions() {
return this.args.canEdit && !this.args.user.locked && this.isActive;
}
<template>
<div class="user-card">
<h3>{{@user.name}}</h3>
{{#if this.isActive}}
<span class="status active">Active</span>
{{else}}
<span class="status inactive">Inactive</span>
{{/if}}
{{#if this.showActions}}
<div class="actions">
<button>Edit</button>
<button>Delete</button>
</div>
{{/if}}
</div>
</template>
}
```
## Conditional Lists
Use `{{#if}}` to guard `{{#each}}` and avoid rendering empty states:
```glimmer-js
// app/components/task-list.gjs
import Component from '@glimmer/component';
class TaskList extends Component {
get hasTasks() {
return this.args.tasks?.length > 0;
}
<template>
{{#if this.hasTasks}}
<ul class="task-list">
{{#each @tasks as |task|}}
<li>
{{task.title}}
{{#if task.completed}}
<span class="done">✓</span>
{{/if}}
</li>
{{/each}}
</ul>
{{else}}
<p class="empty-state">No tasks yet</p>
{{/if}}
</template>
}
```
## Avoid Nested Conditionals
**Bad:**
```glimmer-js
{{#if @user}}
{{#if @user.isPremium}}
{{#if @user.hasAccess}}
<PremiumContent />
{{/if}}
{{/if}}
{{/if}}
```
**Good:**
```glimmer-js
// app/components/content-gate.gjs
import Component from '@glimmer/component';
import { cached } from '@glimmer/tracking';
class ContentGate extends Component {
@cached
get canViewPremium() {
return this.args.user?.isPremium && this.args.user?.hasAccess;
}
<template>
{{#if this.canViewPremium}}
<PremiumContent />
{{else}}
<UpgradeCTA />
{{/if}}
</template>
}
```
## Component Switching Pattern
Use conditional rendering for component selection:
```glimmer-js
// app/components/media-viewer.gjs
import Component from '@glimmer/component';
import ImageViewer from './image-viewer';
import VideoPlayer from './video-player';
import AudioPlayer from './audio-player';
import { cached } from '@glimmer/tracking';
class MediaViewer extends Component {
@cached
get mediaType() {
return this.args.media?.type;
}
<template>
{{#if (eq this.mediaType "image")}}
<ImageViewer @src={{@media.url}} />
{{else if (eq this.mediaType "video")}}
<VideoPlayer @src={{@media.url}} />
{{else if (eq this.mediaType "audio")}}
<AudioPlayer @src={{@media.url}} />
{{else}}
<p>Unsupported media type</p>
{{/if}}
</template>
}
```
## Loading States
Pattern for async data with loading/error states:
```glimmer-js
// app/components/data-display.gjs
import Component from '@glimmer/component';
import { Resource } from 'ember-resources';
import { resource } from 'ember-resources';
class DataResource extends Resource {
@tracked data = null;
@tracked isLoading = true;
@tracked error = null;
modify(positional, named) {
this.fetchData(named.url);
}
async fetchData(url) {
this.isLoading = true;
this.error = null;
try {
const response = await fetch(url);
this.data = await response.json();
} catch (e) {
this.error = e.message;
} finally {
this.isLoading = false;
}
}
}
class DataDisplay extends Component {
@resource data = DataResource.from(() => ({
url: this.args.url,
}));
<template>
{{#if this.data.isLoading}}
<div class="loading">Loading...</div>
{{else if this.data.error}}
<div class="error">Error: {{this.data.error}}</div>
{{else}}
<div class="content">
{{this.data.data}}
</div>
{{/if}}
</template>
}
```
## Performance Impact
- **Chained if/else**: 40-60% faster than multiple independent {{#if}} blocks
- **Extracted getters**: ~20% faster for complex conditions (cached)
- **Component switching**: Same performance as {{#if}} but better code organization
## When to Use
- **{{#if}}/{{#else}}**: For simple true/false conditions
- **Extracted getters**: For complex or reused conditions
- **Component switching**: For different component types based on state
- **Guard clauses**: To avoid rendering large subtrees when not needed
## References
- [Ember Guides - Conditionals](https://guides.emberjs.com/release/components/conditional-content/)
- [Glimmer VM Performance](https://github.com/glimmerjs/glimmer-vm)
- [@cached decorator](https://api.emberjs.com/ember/release/functions/@glimmer%2Ftracking/cached)