RxJS-Powered Presenters: Encapsulating UI Logic in Angular

Presenters act as component-level services designed to house complex presentation and interaction logic. They can be written without any framework dependencies, which allows for consistent user interface behavior across different applications, platforms, and devices. These services are provided and consumed directly within our presentational and mixed components.

These presenters remain largely decoupled from the rest of the application. They typically have no external dependencies, unless they are composite presenters that coordinate other presenters. This isolation makes them remarkably simple to test in a standalone environment, without needing a DOM or even Angular itself, provided we structure them thoughtfully.

Their primary duties include formatting the presented application state, managing ephemeral local UI state, and exposing an interface for handling user interactions.

The application state that gets presented can be supplied to presenters as RxJS observables, standard properties, or even as methods that receive a component's @Input() values.

After exploring the various presenter types and how they can be paired with components, we will circle back to refactor the HeroesComponent from the Tour of Heroes tutorial.

Our final goal is to achieve the control flow depicted in Figure 1.

Figure 1. The control flow after extracting a presenter from the presentational heroes component.

Figure 1. The control flow after extracting a presenter from the presentational heroes component.

Managing Local and Application State

Both presentational components and their associated presenters remain agnostic about the origin of the underlying application state. They focus on maintaining a synchronized snapshot of any relevant data that their consuming components rely on.

Stateful presenters are versatile enough to handle persistent data, client-side state, transient client information, and localized UI flags. This state can be exposed either as straightforward properties or as reactive observable properties.

Transforming Data Without State

In contrast, a stateless presenter does not manage any local UI state through properties, subjects, or other observable types. Instead, its primary function is to transform data, which makes it ideal for formatting tasks but unsuitable for handling user interaction.

Since we typically offload local UI state to presenters, relying on a single stateless presenter is rarely sufficient to cover all the responsibilities of a complex component.

Strategies for Presenter and Component Layout

There is no strict mandate on how many presenters a component should use; the composition is entirely up to the developer. Let's examine the possible ratios and the scenarios where each proves most useful.

The Dedicated Single Presenter

When building a component for a specific use case, the natural starting point is a single, dedicated presenter. This establishes a 1:1 component-to-presenter ratio.

When a component's logic becomes unwieldy, extracting it into a dedicated presenter is a prudent initial refactoring step. As the component's features expand, breaking it down into smaller, focused child components—each with its own 1:1 presenter—becomes a viable path.

A composite presenter acts as a facade that coordinates the work of multiple underlying presenters. It can be tailored to a specific component or to a general behavioral pattern. When this composite is component-specific, it frequently maintains a 1:1 ratio with its component.

Leveraging Multiple Presenters

As a project matures, opportunities for code reuse across features become more apparent. At this stage, a single component might orchestrate several presenters, resulting in a 1:n ratio.

These multiple presenters can be dedicated to the same use case but segregated by concern. For instance, a single component might have one presenter for formatting logic and a separate one for handling behavioral interaction.

Another scenario involves a component with a specific template section that has tightly coupled operations spanning both formatting and behavior. While a single presenter could be crafted to manage both, this is often a signal that the logic is better suited for a dedicated child component rather than remaining embedded in the parent via a presenter.

Sharing One Presenter Across Components

Architecture can also dictate that a single presenter is responsible for distributing state and coordinating interactions across several components. This is an n:1 component-to-presenter ratio.

A stateless presenter, given its nature, can be easily shared. Multiple instances of the same component can even use the same presenter definition, provided they manage independent local state, which would actually shift the architecture back to a 1:1 or 1:n ratio due to instantiation needs.

A prime example of a shared presenter is the controller for a complex data table. The container component would feed application state to this presenter and translate user commands into service calls. Meanwhile, individual row or cell components could each own their own presenters to manage UI specifics like validation and formatting.

The single, table-wide presenter serves as the central hub, distributing state down to the row and cell components and their respective presenters. It also aggregates user interaction events bubbling up from these lower-level components.

Choosing Between a Component and a Presenter

Given that a single component can host multiple presenters for varied concerns, it raises the question: why not create an entirely new component for this logic instead?

One primary constraint is the DOM. Sometimes, adding more elements is impossible due to the strict structure required by valid HTML or third-party libraries. Since Angular allows only one component per element, a single component might be forced to manage multiple presenters to work around this limitation.

Alternative solutions to a rigid DOM include using container directives or provider directives, which we will delve into in a separate piece.

But if we can add DOM elements, when might a component still be a better choice than a presenter?

Consider a search presenter similar to the one in Listing 1.

// search.presenter.ts
import { OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';

export class SearchPresenter implements OnDestroy {
  private searchQuery = new Subject<string>();

  searchQuery$ = this.searchQuery.asObservable();

  ngOnDestroy(): void {
    this.searchQuery.complete();
  }

  search(query: string): void {
    this.searchQuery.next(query);
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 1. Search presenter.

This reusable presenter can be integrated into any component that requires search functionality.

The benefit is centralization. To modify search behavior—like adding debouncing to filter out rapid, duplicate keystrokes—we only need to edit this one file, as demonstrated in Listing 2.

// search.presenter.ts
import { OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

export class SearchPresenter implements OnDestroy {
  private searchQuery = new Subject<string>();

  searchQuery$ = this.searchQuery.pipe(
    debounceTime(150), // 👈
    distinctUntilChanged(), // 👈
  );

  ngOnDestroy(): void {
    this.searchQuery.complete();
  }

  search(query: string): void {
    this.searchQuery.next(query);
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 2. Search presenter with debounced, distinct search query.

As a demonstration, let's bind this presenter to a dedicated search box component, as shown in Listing 3.

// search-box.component.ts
import { Component, EventEmitter, OnInit, Output } from '@angular/core';

import { SearchPresenter } from './search.presenter';

@Component({
  providers: [SearchPresenter],
  selector: 'app-search-box',
  template: `
    <input
      type="search"
      placeholder="Search..."
      (input)="onSearch($event.target.value)"> <!-- [1] -->
  `,
})
export class SearchBoxComponent implements OnInit {
  @Output()
  search = new EventEmitter<string>();

  constructor(
    private presenter: SearchPresenter,
  ) {}

  ngOnInit(): void {
    this.presenter.searchQuery$.subscribe(searchQuery => // [4]
      this.search.emit(searchQuery)); // [4]
  }

  onSearch(query: string): void { // [2]
    this.presenter.search(query); // [3]
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 3. Search box component using search presenter.

We intentionally maintain unidirectional data flow. User queries (1) trigger the component's event handler (2), which feeds them into the presenter (3). The presenter's search observable is then linked to the component's @Output() property (4), enabling parent components to react via event binding.

We have now tightly coupled the search presenter to this specific search box. If this is the only place we need search, it's more efficient to reuse the component. Parent elements can simply add the search box and listen to its search event without dealing with the underlying presenter logic.

However, if we anticipate variations in search behavior across different use cases, reusing the presenter provides greater flexibility.

Obviously, adopting this approach necessitates duplicating the glue code from Listing 3 in every component that needs search. The advantage is the ability to append additional reactive operators to the query stream. These enhancements can be placed directly in a component or delegated to a specialized composite presenter.

In summary, choose to reuse a component (and its presenter) when there is a strong, immutable link between its logic and a specific DOM structure, and when you are confident that the behavior will remain identical across all usage points.

Deciding Between a Pipe and a Presenter

In typical usage, we pass a value to a presenter method for transformation. Or, we might push an observable through a series of operators before subscribing in the template with the async pipe or NgRx's push pipe.

Using a transforming method executes that function on every dirty check, which could be a bottleneck for expensive operations. Although, one could potentially implement memoization to cache and reuse results for the same input.

The performance hit might be negligible for presentational components, as they only undergo dirty checking when their inputs change. However, this is not a guarantee if inputs update frequently.

A memoized pipe, in contrast, caches all historical transform results, providing a constant-time lookup for repeated calls.

Even a standard pure Angular pipe offers a degree of efficiency; Angular short-circuits the evaluation if the input and parameters haven't changed since the last check. It acts as a memoized pipe with a very small cache.

Therefore, in performance-sensitive areas, opting for a pure or memoized pipe over a presenter is a valid strategy.

This choice introduces trade-offs. Pipes are inherently granular—they process a single value, which can make them difficult to unit test in isolation from the broader feature logic, often forcing a test to go through the DOM to verify integration.

Another consideration is the boilerplate overhead. Angular pipes require registration in module declarations and exports, alongside a unique name string, which is then used in templates. This is a larger setup footprint compared to a simple presenter.

Most critically, pipes are limited to transforming data. They do not offer any mechanism for orchestrating user interaction or managing UI-driven state changes.

Simple example

In "Presentational components with Angular", we shifted presentational logic from the heroes component template into its model to reduce template complexity.

// heroes.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormControl } from '@angular/forms';

import { Hero } from '../hero';

@Component({
  selector: 'app-heroes-ui',
  styleUrls: ['./heroes.component.css'],
  templateUrl: './heroes.component.html',
})
export class HeroesComponent {
  @Input()
  heroes: Hero[];
  @Input()
  title: string;

  @Output()
  add = new EventEmitter<string>();
  @Output()
  remove = new EventEmitter<Hero>();

  nameControl = new FormControl('');

  addHero(): void {
    let name = this.nameControl.value;
    this.nameControl.setValue(''); // [2]
    name = name.trim(); // [1]

    if (!name) { // [1]
      return;
    }

    this.add.emit(name);
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 4. Heroes: Presentational component model with form validation and UI behaviour.

Listing 4 highlights intricate user interaction logic for form validation (1) and UI behaviour (2) embedded in the addHero method.

Extract complex presentational logic into a presenter

We'll build a heroes presenter by pulling the elaborate presentational logic out of the presentational component.

// heroes.presenter.ts
import { FormControl } from '@angular/forms';

export class HeroesPresenter {
  nameControl = new FormControl(''); // [2]

  addHero(): void { // [1]
    const name = this.nameControl.value.trim();
    this.nameControl.setValue(''); // [3]

    if (!name) {
      return;
    }

    this.add.emit(name); // [4]
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 5. Heroes: Presenter with extracted form control and related method.

The addHero method (1) gets moved into a component-specific presenter named HeroesPresenter.

Because the addHero method manages UI behaviour by resetting the form control (3), the name form control must also be included within the presenter (2).

The previously final line of the method emitted a value through a component output property (4), and that line is currently non-functional.

We could attach an Angular event emitter to this presenter, but our preference is to keep presenters framework-agnostic where it's practical. So, we opt for an RxJS subject, as shown in Listing 6. Also, an event emitter would have to be typed as an Observable the moment we introduced any operators on top of it.

// heroes.presenter.ts
import { FormControl } from '@angular/forms';
import { Observable, Subject } from 'rxjs';

export class HeroesPresenter {
  private add = new Subject<string>(); // 👈

  add$: Observable<string> = this.add.asObservable(); // 👈
  nameControl = new FormControl('');

  addHero(): void {
    const name = this.nameControl.value.trim();
    this.nameControl.setValue('');

    if (!name) {
      return;
    }

    this.add.next(name);
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 6. Heroes: Presenter with subject exposed as observable.

The presenter now exposes an observable add$ property that the presentational component can bind to.

API design tip: Subjects and event emitters should not be exposed unless they serve as component output properties.

Inject the presenter into the presentational component

Our goal is to inject the heroes presenter into the constructor of the presentational component. To make that possible, we register it as a component-level service, as demonstrated in Listing 7.

// heroes.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';

import { Hero } from '../hero';
import { HeroesPresenter } from './heroes.presenter';

@Component({
  providers: [HeroesPresenter], // 👈
  selector: 'app-heroes-ui',
  styleUrls: ['./heroes.component.css'],
  templateUrl: './heroes.component.html',
})
export class HeroesComponent {
  @Input()
  heroes: Hero[];
  @Input()
  title: string;

  @Output()
  add = new EventEmitter<string>();
  @Output()
  remove = new EventEmitter<Hero>();

  constructor(
    private presenter: HeroesPresenter,
  ) {}

  addHero(): void {}
}
Enter fullscreen mode Exit fullscreen mode
Listing 7. Heroes: Presentational component with presenter.

Adding the presenter to the providers component option scopes it to the component instance. This means the presenter's lifecycle mirrors the component's: it is instantiated just prior to the presentational component and is destroyed right before the component perishes.

Delegate UI properties and event handlers to the presenter

With the presentational heroes component now having access to the presenter, we can hand off UI properties and event handler responsibilities.

// heroes.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormControl } from '@angular/forms';

import { Hero } from '../hero';
import { HeroesPresenter } from './heroes.presenter';

@Component({
  providers: [HeroesPresenter],
  selector: 'app-heroes-ui',
  styleUrls: ['./heroes.component.css'],
  templateUrl: './heroes.component.html',
})
export class HeroesComponent {
  @Input()
  heroes: Hero[];
  @Input()
  title: string;

  @Output()
  add = new EventEmitter<string>();
  @Output()
  remove = new EventEmitter<Hero>();

  get nameControl(): FormControl {
    return this.presenter.nameControl; // 👈
  }

  constructor(
    private presenter: HeroesPresenter,
  ) {}

  addHero(): void {
    this.presenter.addHero(); // 👈
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 8. Heroes: Presentational component delegating UI property and event handler to its presenter.

As Listing 8 shows, the heroes component introduces a nameControl getter that forwards to the presenter. It also redirects its addHero event handler to call the presenter's addHero method.

Connect the presenter to the presentational component's data binding API

A couple of steps remain to wrap up this refactoring. First, we need to hook the presenter's observable property up to the component's output property.

// heroes.component.ts
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormControl } from '@angular/forms';

import { Hero } from '../hero';
import { HeroesPresenter } from './heroes.presenter';

@Component({
  providers: [HeroesPresenter],
  selector: 'app-heroes-ui',
  styleUrls: ['./heroes.component.css'],
  templateUrl: './heroes.component.html',
})
export class HeroesComponent implements OnInit {
  @Input()
  heroes: Hero[];
  @Input()
  title: string;

  @Output()
  add = new EventEmitter<string>();
  @Output()
  remove = new EventEmitter<Hero>();

  get nameControl(): FormControl {
    return this.presenter.nameControl;
  }

  constructor(
    private presenter: HeroesPresenter,
  ) {}

  ngOnInit(): void {
    this.presenter.add$.subscribe(name => this.add.emit(name)); // 👈
  }

  addHero(): void {
    this.presenter.addHero();
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 9A. Heroes: Presentational component with its data binding API connected to its presenter.

In Listing 9A, we subscribe to the presenter's add$ observable and forward emitted values to the heroes component's add output property.

// heroes.component.ts
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormControl } from '@angular/forms';

import { Hero } from '../hero';
import { HeroesPresenter } from './heroes.presenter';

@Component({
  providers: [HeroesPresenter],
  selector: 'app-heroes-ui',
  styleUrls: ['./heroes.component.css'],
  templateUrl: './heroes.component.html',
})
export class HeroesComponent implements OnInit {
  @Input()
  heroes: Hero[];
  @Input()
  title: string;

  @Output()
  add = new EventEmitter<string>();
  @Output()
  remove = new EventEmitter<Hero>();

  get nameControl(): FormControl {
    return this.presenter.nameControl;
  }

  constructor(
    private presenter: HeroesPresenter,
  ) {}

  ngOnInit(): void {
    this.presenter.add$.subscribe(this.add); // 👈
  }

  addHero(): void {
    this.presenter.addHero();
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 9B. Heroes: Presentational component with its data binding API connected to its presenter.

An alternative approach, seen in Listing 9B, connects the presenter to the output property by subscribing the output property directly to the add$ observable.

Rather than using an event emitter, we could have assigned a component getter marked as an output property to delegate to the presenter's observable. That works because an output property only needs a subscribe method like an observable or subject. Still, we'll stick with Angular's conventional building blocks inside components.

If our presenter held presentational transformation methods, like formatting utilities, we'd add component methods or getters that feed input properties into those methods. We might also define component input properties whose setters pass values to the presenter. Those values would then drive component UI properties that delegate to presenter getters or methods.

Is anything missing? What about managing the connecting subscription in the heroes component?

Manage observable subscriptions

Had we used the presenter's observable as a component output property, Angular would handle the subscription lifecycle automatically.

We have three choices for managing the subscription ourselves.

// heroes.component.ts
import { Component, EventEmitter, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { HeroesPresenter } from './heroes.presenter';

@Component({
  selector: 'app-heroes-ui',
})
export class HeroesComponent implements OnDestroy, OnInit {
  private destroy = new Subject<void>(); // 👈

  @Output()
  add = new EventEmitter<string>();

  constructor(
    private presenter: HeroesPresenter,
  ) {}

  ngOnInit(): void {
    this.presenter.add$.pipe(
      takeUntil(this.destroy), // 👈
    ).subscribe(name => this.add.emit(name));
  }

  ngOnDestroy(): void { // 👈
    this.destroy.next();
    this.destroy.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 10A. Heroes: Component managing subscription using a lifecycle subject.

The first option adds a private destroy subject to the component, paired with the takeUntil operator and fired in the OnDestroy lifecycle hook, as shown in Listing 10A. This pattern is likely familiar.

// heroes.component.ts
import { Component, EventEmitter, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';

import { HeroesPresenter } from './heroes.presenter';

@Component({
  selector: 'app-heroes-ui',
})
export class HeroesComponent implements OnDestroy, OnInit {
  private subscription: Subscription; // 👈

  @Output()
  add = new EventEmitter<string>();

  constructor(
    private presenter: HeroesPresenter,
  ) {}

  ngOnInit(): void {
    this.subscription = this.presenter.add$.subscribe(name =>
      this.add.emit(name));
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe(); // 👈
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 10B. Heroes: Component managing subscription using a subscription object.

A second path stores the subscription in a private property and unsubscribes in the component's OnDestroy hook, as seen in Listing 10B. This follows classic RxJS practice.

The last option lets the presenter govern subscriptions that rely on it by completing the add subject in its OnDestroy hook. Compared to the previous two, this requires the least code.

// heroes.presenter.ts
import { OnDestroy } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable, Subject } from 'rxjs';

export class HeroesPresenter implements OnDestroy {
  private add = new Subject<string>();

  add$: Observable<string> = this.add.asObservable();
  nameControl = new FormControl('');

  ngOnDestroy(): void {
    this.add.complete(); // 👈
  }

  addHero(): void {
    const name = this.nameControl.value.trim();
    this.nameControl.setValue('');

    if (!name) {
      return;
    }

    this.add.next(name);
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 10C. Heroes: Presenter managing subscribers.

Listing 10C shows an ngOnDestroy lifecycle hook added to the presenter that completes the private add subject. Completing a subject or any observable triggers each subscriber's complete hook, if present, and then unsubscribes them.

That said, caution is needed with a shared stateful presenter. If the components have different lifecycles—activated and destroyed at distinct times—subscriptions could persist for components already torn down.

Subscription management rule: Relying on the presenter to manage subscriptions is only safe when the presenter is shared among one or more components that are activated and destroyed simultaneously.

When sharing a presenter across routed components, components with dynamic rendering, or structural directives, we should fall back to one of the traditional subscription management approaches.

An even stronger approach combines both strategies: the presenter and the subscribing components each end their own subscriptions. This aids cleanup in unit tests and reduces the chance of memory leaks.

Refinements to consider

There is no such thing as a perfect implementation. The following suggestions may help you adapt the pattern to your needs.

Reworking the heroes presenter

A key benefit of the presenter pattern is the ability to change its internal workings or add new presentation logic without altering the public contract it exposes.

Looking at the heroes presenter, the extracted UI state and form validation logic is exclusively concerned with the add hero form. This focus suggests a potential rename.

Renaming it to HeroForm would still leave it as a valid presenter. This new name would signal that it isn't tied to a single component but might be reusable across them, and that it could be one of several presenters, each handling a distinct concern.

The imperative style of the addHero method is a hint that a more declarative, reactive pipeline could be a better fit.

// heroes.presenter.ts
import { FormControl } from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';

export class HeroesPresenter {
  private add = new Subject<string>();

  add$: Observable<string> = this.add.pipe(
    map(name => name.trim()), // 👈
    filter(name => !!name), // 👈
  );
  nameControl = new FormControl('');

  addHero(): void {
    const name = this.nameControl.value;
    this.nameControl.setValue('');

    this.add.next(name);
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 11. Heroes: Presenter with input sanitising and validation in observable pipeline.

Listing 11 demonstrates how sanitizing and validation can be expressed through RxJS operators. While Angular's Reactive Forms offer an even more concise way to define such dataflows, that is a topic for a future discussion.

Enforcing stricter dependency injection

Angular's DI is robust, but it can inadvertently expose internal dependencies to other components and directives if not managed carefully.

By placing the heroes presenter in the providers array, we enable injection into the host component, but we also grant the same access to all its view children, content children, and their descendants. This is useful when we intend to share a presenter, as noted in the "Component-to-presenter ratios" section. However, it may not be desirable to expose the service to projected content.

In our example, no content is projected. Should that change, using viewProviders instead would prevent the service from leaking to any declarables that are not part of the component's own view, which is often a safer default.

Presenter injection tip: Prefer the viewProviders option when providing a presenter unless you specifically need to share it with content children.

Another layer of protection is to only expose a factory for the service, not the service itself.

// heroes-presenter-factory.token.ts
import { InjectionToken } from '@angular/core';

import { HeroesPresenter } from './heroes.presenter';

export const heroesPresenterFactoryToken = new InjectionToken(
  'Heroes presenter factory', {
    factory: (): (() => HeroesPresenter) =>
      () => new HeroesPresenter(),
  });
Enter fullscreen mode Exit fullscreen mode
Listing 12A. Heroes: Dependency injection token for presenter service factory.
// heroes.presenter.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
  useFactory: (): never => {
    throw new Error('Use heroesPresenterFactoryToken to create a hero presenter.');  },
})
export class HeroesPresenter {}
Enter fullscreen mode Exit fullscreen mode
Listing 12B. Heroes: Presenter provider guarding direct injection.
// heroes.component.ts
import { Component, Inject, OnDestroy } from '@angular/core';

import { HeroesPresenter } from './heroes.presenter';
import { heroesPresenterFactoryToken } from './heroes-presenter-factory.token';

@Component({
  providers: [
    {
      deps: [
        [new Inject(heroesPresenterFactoryToken)],
      ],
    provide: HeroesPresenter,
      useFactory:
        (createHeroesPresenter: () => HeroesPresenter): HeroesPresenter =>
          createHeroesPresenter(),
    },
  ],
  selector: 'app-heroes-ui',
})
export class HeroesComponent implements OnDestroy {
  constructor(
    private presenter: HeroesPresenter,
  ) {}
}
Enter fullscreen mode Exit fullscreen mode
Listing 12C. Heroes: Presentational component using presenter service factory.

Listings 12A, 12B, and 12C illustrate how a service factory can be used to instantiate the heroes presenter. The provider for the presenter itself would throw an error if any other declarable attempted to inject it directly.

Even if another component were to inject the factory, it would get a fresh instance of the presenter, preventing any accidental sharing.

The provider from Listing 12C can be exported from the module that declares the injection token for convenient reuse.

Finally, we can enforce strict DI rules by applying the Self decorator factory at the point of injection in the presentational component. Without a factory, the code would resemble Listing 13.

// heroes.component.ts
import { Component, Self } from '@angular/core';

import { HeroesPresenter } from './heroes.presenter';

@Component({
  selector: 'app-heroes-ui',
})
export class HeroesComponent {
  constructor(
    @Self() private presenter: HeroesPresenter,
  ) {}
}
Enter fullscreen mode Exit fullscreen mode
Listing 13. Heroes: Enforcing presenter injection from own node injector.

The Self decorator factory tells Angular to resolve the dependency only from the component's own node injector, blocking access to any provider higher up the tree.

Presenter injection tip: Use the Self decorator factory where the presenter is injected unless it is a shared presenter. This prevents accidental injection of an ancestor component's presenter.

Leveraging observable properties as outputs

While EventEmitter is the traditional choice for component outputs, Angular only requires an object with a subscribe method that accepts an observer. This opens the door to using observables directly.

Since presenters already expose observables, we can delegate these to the component's outputs as shown in Listings 14A and 14B.

// heroes.component.ts
import { Component, Output } from '@angular/core';

import { HeroesPresenter } from './heroes.presenter';

@Component({
  providers: [HeroesPresenter],
  selector: 'app-heroes-ui',
  styleUrls: ['./heroes.component.css'],
  templateUrl: './heroes.component.html',
})
export class HeroesComponent {
  @Output('add')
  get add$(): Observable<string> { // 👈
    return this.presenter.add$;
  }

  constructor(
    private presenter: HeroesPresenter,
  ) {}

  addHero(): void {
    this.presenter.addHero();
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 14A. Heroes: Presentational component delegating an output property to its presenter using a getter.
// heroes.component.ts
import { Component, Output } from '@angular/core';

import { HeroesPresenter } from './heroes.presenter';

@Component({
  providers: [HeroesPresenter],
  selector: 'app-heroes-ui',
  styleUrls: ['./heroes.component.css'],
  templateUrl: './heroes.component.html',
})
export class HeroesComponent {
  @Output('add')
  add$ = this.presenter.add$; // 👈

  constructor(
    private presenter: HeroesPresenter,
  ) {}

  addHero(): void {
    this.presenter.addHero();
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 14B. Heroes: Presentational component delegating an output property to its presenter using a property reference.

In both Listings 13A and 13B, we eliminate the need to manually bridge the presenter's observable to the component's emitter, thereby removing the OnInit lifecycle hook entirely.

Building framework-agnostic presenters

For projects that target multiple frameworks or platforms, keeping presenters free of any framework-specific dependencies is a prudent strategy.

// heroes.presenter.ts
import { Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';

export class HeroesPresenter {
  private add = new Subject<string>();

  add$: Observable<string> = this.add.pipe(
    map(name => name.trim()), // [2]
    filter(name => !!name), // [2]
  );

  destroy(): void { // [1]
    this.add.complete();
  }

  addHero(name: string): void {
    this.add.next(name);
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 15A. Framework-agnostic heroes presenter.

Listing 15A shows a version of the heroes presenter that is framework-agnostic. The Angular-specific ngOnDestroy lifecycle hook is replaced by a plain destroy method (1).

We also remove the FormControl. While Angular's forms are a solid library that could be reused elsewhere, the input sanitizing and validation are now handled directly within the observable pipeline (2).

// app-heroes.presenter.ts
import { Injectable, OnDestroy } from '@angular/core';
import { FormControl } from '@angular/forms';

import { HeroesPresenter } from './heroes.presenter';

@Injectable()
export class AppHeroesPresenter implements OnDestroy {
  add$ = this.presenter.add$; // [3]
  nameControl = new FormControl('');

  constructor(
    private presenter: HeroesPresenter, // [1]
  ) {}

  ngOnDestroy(): void {
    this.presenter.destroy(); // [2]
  }

  addHero(): void {
    const name = this.nameControl.value;
    this.nameControl.setValue(''); // [5]

    this.presenter.addHero(name); // [4]
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 15B. Angular-specific presenter wrapping the framework-agnostic heroes presenter.

Listing 15B presents the Angular-specific wrapper around the generic presenter from Listing 15A. It injects the base presenter (1) and calls its destroy method from the ngOnDestroy lifecycle hook (2).

This wrapper also forwards the add$ observable from the base presenter (3) and adds a FormControl that pipes each value into the base presenter's addHero method (4), while retaining the logic to reset the control (5).

Because the Angular-specific presenter maintains the same public API, its integration into a component is identical to the previous examples.

Defining characteristics of presenters

A presenter is a potentially reusable class, largely decoupled from the rest of the application, with few or no dependencies—except possibly other presenters in a composite setup. They are registered at the component level in providers and consumed by presentational or mixed components.

The application state managed by a presenter can be exposed as RxJS observables, regular properties, or methods. Components pass inputs through these to format data for the user via the template.

Stateful presenters manage a synchronized, local representation of a piece of application state, commonly local UI state as properties or observables.

Stateless presenters focus on data transformation, deliberately avoiding any logic related to user interaction.

The component-to-presenter ratio is flexible. A single presenter per component, like a composite, is a valid approach.

Alternatively, a component could use multiple presenters, each addressing a different concern, such as interactions or formatting. Conversely, a single stateless presenter can be shared across many components without issue.

When there is strong cohesion between the presenter logic and a specific slice of the DOM, extracting a component instead can be a better move, provided the UI behavior doesn't vary based on context.

For performance-sensitive formatting, a pure pipe or a memoized pipe may be preferable to a presenter. Remember that pipes are tightly scoped and require significant boilerplate.

Pipes also cannot handle any UI interaction logic.

It bears repeating that presenters can be built without any framework dependencies, ensuring consistent UI behavior across different apps, platforms, and devices.

A major advantage is testability. Presenters can be tested in complete isolation, without a UI, and if well-designed, without any framework- or platform-specific code.

Extracting a presenter from a component

The extraction process can be broken down into a few simple steps:

  1. Move complex presentation logic into a dedicated presenter.
  2. Inject that presenter into the component.
  3. Wire the presenter to the component's template and inputs/outputs.
  4. Handle all observable subscriptions appropriately.

After extraction, the component's template and public API should largely remain unchanged, though there might be some adjustments to UI-bound properties.

The result is a set of presenters that encapsulate these core concerns:

  • Data formatting and transformation for display
  • Local UI state management and behavior
  • Validation of form inputs
  • Handling and emitting application-specific events

For an introduction to the pattern, read “Model-View-Presenter with Angular”. That article also contains the link to the companion GitHub repository and other relevant resources.

To understand how to turn a mixed component into a presentational one, see "Presentational components with Angular".

Acknowledgments

I want to express my gratitude to the experts who offered their guidance and feedback while reviewing this manuscript.