--- 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; } }; } ``` **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); } } ``` **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); } } ``` **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)); } } ``` 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)