---
title: Template-Only Components with In-Scope Functions
impact: MEDIUM
impactDescription: Clean, performant patterns for template-only components
tags: templates, components, functions, performance
---
## Template-Only Components with In-Scope Functions
For template-only components (components without a class and `this`), use in-scope functions to keep logic close to the template while avoiding unnecessary caching overhead.
**Incorrect (using class-based component for simple logic):**
```glimmer-js
// app/components/product-card.gjs
import Component from '@glimmer/component';
export class ProductCard extends Component {
// Unnecessary class and overhead for simple formatting
formatPrice(price) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(price);
}
{{@product.name}}
{{this.formatPrice @product.price}}
}
```
**Correct (template-only component with in-scope functions):**
```glimmer-js
// app/components/product-card.gjs
function formatPrice(price) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(price);
}
function calculateDiscount(price, discountPercent) {
return price * (1 - discountPercent / 100);
}
function isOnSale(product) {
return product.discountPercent > 0;
}
```
**When to use class-based vs template-only:**
```glimmer-js
// Use class-based when:
// - You need @cached for expensive computations accessed multiple times
// - You have tracked state
// - You need lifecycle hooks or services
import Component from '@glimmer/component';
import { cached } from '@glimmer/tracking';
export class ProductList extends Component {
@cached
get sortedProducts() {
// Expensive sort, accessed in template multiple times
return [...this.args.products].sort((a, b) => a.name.localeCompare(b.name));
}
@cached
get filteredProducts() {
// Depends on sortedProducts - benefits from caching
return this.sortedProducts.filter((p) => p.category === this.args.selectedCategory);
}
{{#each this.filteredProducts as |product|}}
{{product.name}}
{{/each}}
}
```
```glimmer-js
// Use template-only when:
// - Simple transformations
// - Functions accessed once
// - No state or services needed
function formatDate(date) {
return new Date(date).toLocaleDateString();
}
```
**Key differences from class-based components:**
| Aspect | Template-Only | Class-Based |
| ---------------- | ------------------------ | ------------------------ |
| `this` context | ❌ No `this` | ✅ Has `this` |
| Function caching | ❌ Recreated each render | ✅ `@cached` available |
| Services | ❌ Cannot inject | ✅ `@service` decorator |
| Tracked state | ❌ No instance state | ✅ `@tracked` properties |
| Best for | Simple, stateless | Complex, stateful |
**Best practices:**
1. **Keep functions simple** - If computation is complex, consider a class with `@cached`
2. **One responsibility per function** - Makes them reusable and testable
3. **Minimize nesting** - Use intermediate functions for readability
4. **No side effects** - Functions should be pure transformations
5. **Export for testing** - Export functions so they can be tested independently
```glimmer-js
// app/components/stats-display.gjs
export function average(numbers) {
if (numbers.length === 0) return 0;
return numbers.reduce((sum, n) => sum + n, 0) / numbers.length;
}
export function round(number, decimals = 2) {
return Math.round(number * Math.pow(10, decimals)) / Math.pow(10, decimals);
}
Average:
{{round (average @scores)}}
```
Reference: [Template-only Components](https://guides.emberjs.com/release/components/component-types/), [Component Authoring Best Practices](https://guides.emberjs.com/release/components/conditional-content/)