---
title: Use Class Fields for Component Composition
impact: MEDIUM-HIGH
impactDescription: Better composition and initialization patterns
tags: components, class-fields, composition, initialization
---
## Use Class Fields for Component Composition
Use class fields for clean component composition, initialization, and dependency injection patterns. Tracked class fields should be **roots of state** - representing the minimal independent state that your component owns. In most apps, you should have very few tracked fields.
**Incorrect (imperative initialization, scattered state):**
```glimmer-js
// app/components/data-manager.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service';
class DataManager extends Component {
@service store;
@service router;
// Scattered state management - hard to track relationships
@tracked currentUser = null;
@tracked isLoading = false;
@tracked error = null;
loadData = async () => {
this.isLoading = true;
try {
this.currentUser = await this.store.request({ url: '/users/me' });
} catch (e) {
this.error = e;
} finally {
this.isLoading = false;
}
};
{{this.currentUser.name}}
}
```
**Correct (class fields with proper patterns):**
```glimmer-js
// app/components/data-manager.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service';
import { cached } from '@glimmer/tracking';
import { getPromiseState } from '@warp-drive/reactiveweb';
class DataManager extends Component {
// Service injection as class fields
@service store;
@service router;
// Tracked state as class fields - this is a "root of state"
// Most components should have very few of these
@tracked selectedFilter = 'all';
// Data loading with getPromiseState
@cached
get currentUser() {
const promise = this.store.request({
url: '/users/me',
});
return getPromiseState(promise);
}
{{#if this.currentUser.isFulfilled}}
{{this.currentUser.value.name}}
{{else if this.currentUser.isRejected}}
Error: {{this.currentUser.error.message}}
{{/if}}
}
```
**Understanding "roots of state":**
Tracked fields should represent **independent state** that your component owns - not derived data or loaded data. Examples of good tracked fields:
- User selections (selected tab, filter option)
- UI state (is modal open, is expanded)
- Form input values (not yet persisted)
In most apps, you'll have very few tracked fields because most data comes from arguments, services, or computed getters.
**Composition through class field assignment:**
```glimmer-js
// app/components/form-container.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { TrackedObject } from 'tracked-built-ins';
class FormContainer extends Component {
// Compose form state
@tracked formData = new TrackedObject({
firstName: '',
lastName: '',
email: '',
preferences: {
newsletter: false,
notifications: true,
},
});
// Compose validation state
@tracked errors = new TrackedObject({});
// Compose UI state
@tracked ui = new TrackedObject({
isSubmitting: false,
isDirty: false,
showErrors: false,
});
// Computed field based on composed state
get isValid() {
return Object.keys(this.errors).length === 0 && this.formData.email && this.formData.firstName;
}
get canSubmit() {
return this.isValid && !this.ui.isSubmitting && this.ui.isDirty;
}
updateField = (field, value) => {
this.formData[field] = value;
this.ui.isDirty = true;
this.validate(field, value);
};
validate(field, value) {
if (field === 'email' && !value.includes('@')) {
this.errors.email = 'Invalid email';
} else {
delete this.errors[field];
}
}
}
```
**Mixin-like composition with class fields:**
```javascript
// app/utils/pagination-mixin.js
import { tracked } from '@glimmer/tracking';
export class PaginationState {
@tracked page = 1;
@tracked perPage = 20;
get offset() {
return (this.page - 1) * this.perPage;
}
nextPage = () => {
this.page++;
};
prevPage = () => {
if (this.page > 1) this.page--;
};
goToPage = (page) => {
this.page = page;
};
}
```
```glimmer-js
// app/components/paginated-list.gjs
import Component from '@glimmer/component';
import { cached } from '@glimmer/tracking';
import { PaginationState } from '../utils/pagination-mixin';
class PaginatedList extends Component {
// Compose pagination functionality
pagination = new PaginationState();
@cached
get paginatedItems() {
const start = this.pagination.offset;
const end = start + this.pagination.perPage;
return this.args.items.slice(start, end);
}
get totalPages() {
return Math.ceil(this.args.items.length / this.pagination.perPage);
}
{{#each this.paginatedItems as |item|}}
{{item.name}}
{{/each}}
Page {{this.pagination.page}} of {{this.totalPages}}
}
```
**Shareable state objects:**
```javascript
// app/utils/selection-state.js
import { tracked } from '@glimmer/tracking';
import { TrackedSet } from 'tracked-built-ins';
export class SelectionState {
@tracked selectedIds = new TrackedSet();
get count() {
return this.selectedIds.size;
}
get hasSelection() {
return this.selectedIds.size > 0;
}
isSelected(id) {
return this.selectedIds.has(id);
}
toggle = (id) => {
if (this.selectedIds.has(id)) {
this.selectedIds.delete(id);
} else {
this.selectedIds.add(id);
}
};
selectAll = (items) => {
items.forEach((item) => this.selectedIds.add(item.id));
};
clear = () => {
this.selectedIds.clear();
};
}
```
```glimmer-js
// app/components/selectable-list.gjs
import Component from '@glimmer/component';
import { SelectionState } from '../utils/selection-state';
class SelectableList extends Component {
// Compose selection behavior
selection = new SelectionState();
get selectedItems() {
return this.args.items.filter((item) => this.selection.isSelected(item.id));
}
{{this.selection.count}} selected
{{#each @items as |item|}}
{{item.name}}
{{/each}}
{{#if this.selection.hasSelection}}
{{/if}}
}
```
Class fields provide clean composition patterns, better initialization, and shareable state objects that can be tested independently.
Reference: [JavaScript Class Fields](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields)