Skip to content

Manage Service Owner and Linkage Patterns

Understand how to manage service linkage, owner passing, and alternative service organization patterns beyond the traditional app/services directory.

Owner and Linkage Fundamentals

Incorrect (manual service instantiation):

glimmer-js
// app/components/user-profile.gjs
import Component from '@glimmer/component';
import ApiService from '../services/api';

class UserProfile extends Component {
  // ❌ Creates orphaned instance without owner
  api = new ApiService();

  async loadUser() {
    // Won't have access to other services or owner features
    return this.api.fetch('/user/me');
  }

  <template>
    <div>{{@user.name}}</div>
  </template>
}```

**Correct (proper service injection with owner):**

```glimmer-js
// app/components/user-profile.gjs
import Component from '@glimmer/component';
import { service } from '@ember/service';

class UserProfile extends Component {
  // ✅ Proper injection with owner linkage
  @service api;

  async loadUser() {
    // Has full owner context and can inject other services
    return this.api.fetch('/user/me');
  }

  <template>
    <div>{{@user.name}}</div>
  </template>
}```

### Manual Owner Passing (Without Libraries)

**Creating instances with owner:**

```glimmer-js
// app/components/data-processor.gjs
import Component from '@glimmer/component';
import { getOwner, setOwner } from '@ember/application';
import { service } from '@ember/service';

class DataTransformer {
  @service store;

  transform(data) {
    // Can use injected services because it has an owner
    return this.store.request({ url: '/transform', data });
  }
}

class DataProcessor extends Component {
  @service('store') storeService;

  constructor(owner, args) {
    super(owner, args);

    // Manual instantiation with owner linkage
    this.transformer = new DataTransformer();
    setOwner(this.transformer, getOwner(this));
  }

  processData(data) {
    // transformer can now access services
    return this.transformer.transform(data);
  }

  <template>
    <div>Processing...</div>
  </template>
}```

**Factory pattern with owner:**

```javascript
// app/utils/logger-factory.js
import { getOwner } from '@ember/application';

class Logger {
  constructor(owner, context) {
    this.owner = owner;
    this.context = context;
  }

  get config() {
    // Access configuration service via owner
    return getOwner(this).lookup('service:config');
  }

  log(message) {
    if (this.config.enableLogging) {
      console.log(`[${this.context}]`, message);
    }
  }
}

export function createLogger(owner, context) {
  return new Logger(owner, context);
}
glimmer-js
// Usage in component
import Component from '@glimmer/component';
import { getOwner } from '@ember/application';
import { createLogger } from '../utils/logger-factory';

class My extends Component {
  logger = createLogger(getOwner(this), 'MyComponent');

  performAction() {
    this.logger.log('Action performed');
  }

  <template>
    <button {{on "click" this.performAction}}>Do Something</button>
  </template>
}```

### Owner Passing with Modern Libraries

**Using reactiveweb's link() for ownership and destruction:**

The `link()` function from `reactiveweb` provides both ownership transfer and automatic destruction linkage.

```glimmer-js
// app/components/advanced-form.gjs
import Component from '@glimmer/component';
import { link } from 'reactiveweb/link';

class ValidationService {
  validate(data) {
    // Validation logic
    return data.email && data.email.includes('@');
  }
}

class FormStateManager {
  data = { email: '' };

  updateEmail(value) {
    this.data.email = value;
  }
}

export class AdvancedForm extends Component {
  // link() handles both owner and destruction automatically
  validation = link(this, () => new ValidationService());
  formState = link(this, () => new FormStateManager());

  get isValid() {
    return this.validation.validate(this.formState.data);
  }

  <template>
    <form>
      <input value={{this.formState.data.email}} />
      {{#if (not this.isValid)}}
        <span>Invalid form</span>
      {{/if}}
    </form>
  </template>
}

Why use link():

Services Outside app/services Directory

Using createService from ember-primitives:

glimmer-js
// app/components/analytics-tracker.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { createService } from 'ember-primitives/utils';

// Define service logic as a plain function
function AnalyticsService() {
  let events = [];

  return {
    get events() {
      return events;
    },

    track(event) {
      events.push({ ...event, timestamp: Date.now() });

      // Send to analytics endpoint
      fetch('/analytics', {
        method: 'POST',
        body: JSON.stringify(event)
      });
    }
  };
}

export class AnalyticsTracker extends Component {
  // createService handles owner linkage and cleanup automatically
  analytics = createService(this, AnalyticsService);

  <template>
    <div>Tracking {{this.analytics.events.length}} events</div>
  </template>
}

Why createService:

Co-located services with components:

javascript
// app/components/shopping-cart/service.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { TrackedArray } from 'tracked-built-ins';
import { action } from '@ember/object';

export class CartService extends Service {
  @tracked items = new TrackedArray([]);

  get total() {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }

  @action
  addItem(item) {
    this.items.push(item);
  }

  @action
  removeItem(id) {
    const index = this.items.findIndex(item => item.id === id);
    if (index > -1) this.items.splice(index, 1);
  }

  @action
  clear() {
    this.items.clear();
  }
}
glimmer-js
// app/components/shopping-cart/index.gjs
import Component from '@glimmer/component';
import { getOwner, setOwner } from '@ember/application';
import { CartService } from './service';

class ShoppingCart extends Component {
  cart = (() => {
    const instance = new CartService();
    setOwner(instance, getOwner(this));
    return instance;
  })();

  <template>
    <div class="cart">
      <h3>Cart ({{this.cart.items.length}} items)</h3>
      <div>Total: ${{this.cart.total}}</div>

      {{#each this.cart.items as |item|}}
        <div class="cart-item">
          {{item.name}} - ${{item.price}}
          <button {{on "click" (fn this.cart.removeItem item.id)}}>
            Remove
          </button>
        </div>
      {{/each}}

      <button {{on "click" this.cart.clear}}>Clear Cart</button>
    </div>
  </template>
}```

**Service-like utilities in utils/ directory:**

```javascript
// app/utils/notification-manager.js
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { TrackedArray } from 'tracked-built-ins';
import { setOwner } from '@ember/application';

export class NotificationManager {
  @tracked notifications = new TrackedArray([]);

  constructor(owner) {
    setOwner(this, owner);
  }

  @action
  add(message, type = 'info') {
    const notification = {
      id: Math.random().toString(36),
      message,
      type,
      timestamp: Date.now()
    };

    this.notifications.push(notification);

    // Auto-dismiss after 5 seconds
    setTimeout(() => this.dismiss(notification.id), 5000);
  }

  @action
  dismiss(id) {
    const index = this.notifications.findIndex(n => n.id === id);
    if (index > -1) this.notifications.splice(index, 1);
  }
}
glimmer-js
// app/components/notification-container.gjs
import Component from '@glimmer/component';
import { getOwner } from '@ember/application';
import { NotificationManager } from '../utils/notification-manager';

class NotificationContainer extends Component {
  notifications = new NotificationManager(getOwner(this));

  <template>
    <div class="notifications">
      {{#each this.notifications.notifications as |notif|}}
        <div class="notification notification-{{notif.type}}">
          {{notif.message}}
          <button {{on "click" (fn this.notifications.dismiss notif.id)}}>
            ×
          </button>
        </div>
      {{/each}}
    </div>

    {{! Example usage }}
    <button {{on "click" (fn this.notifications.add "Success!" "success")}}>
      Show Notification
    </button>
  </template>
}```

### Registering Custom Services Dynamically

**Runtime service registration:**

```javascript
// app/instance-initializers/dynamic-services.js
export function initialize(appInstance) {
  // Register service dynamically without app/services file
  appInstance.register('service:feature-flags', class FeatureFlagsService {
    flags = {
      newDashboard: true,
      betaFeatures: false
    };

    isEnabled(flag) {
      return this.flags[flag] || false;
    }
  });

  // Make it a singleton
  appInstance.inject('route', 'featureFlags', 'service:feature-flags');
  appInstance.inject('component', 'featureFlags', 'service:feature-flags');
}

export default {
  initialize
};

Using registered services:

glimmer-js
// app/components/feature-gated.gjs
import Component from '@glimmer/component';
import { service } from '@ember/service';

class FeatureGated extends Component {
  @service featureFlags;

  get shouldShow() {
    return this.featureFlags.isEnabled(this.args.feature);
  }

  <template>
    {{#if this.shouldShow}}
      {{yield}}
    {{else}}
      <div class="feature-disabled">This feature is not available</div>
    {{/if}}
  </template>
}```

### Best Practices

1. **Use @service decorator** for app/services - cleanest and most maintainable
2. **Use link() from reactiveweb** for ownership and destruction linkage
3. **Use createService from ember-primitives** for component-scoped services without extending Service class
4. **Manual owner passing** for utilities that need occasional service access
5. **Co-located services** for component-specific state that doesn't need global access
6. **Runtime registration** for dynamic services or testing scenarios
7. **Always use setOwner** when manually instantiating classes that need services

### When to Use Each Pattern

- **app/services**: Global singletons needed across the app
- **link() from reactiveweb**: When you need both owner and destruction linkage
- **createService from ember-primitives**: Component-scoped services without Service class
- **Co-located services**: Component-specific state, not needed elsewhere
- **Utils with owner**: Stateless utilities that occasionally need config/services
- **Runtime registration**: Dynamic configuration, feature flags, testing

Reference: [Ember Owner API](https://api.emberjs.com/ember/release/functions/@ember%2Fapplication/getOwner), [Dependency Injection](https://guides.emberjs.com/release/applications/dependency-injection/), [reactiveweb link()](https://reactive.nullvoxpopuli.com/functions/link.link.html), [ember-primitives createService](https://ce1d7e18.ember-primitives.pages.dev/6-utils/createService.md)