The new approach to the Container Presenter pattern in Angular

Picture of a container.
Photo by Venti Views on Unsplash

Working in an advanced frontend application requires us to use patterns. The main benefit of them is that they allow many programmers to work on one codebase. Patterns introduce aspects of common language and allow us to better understand how the code works. Some of them are universal like Singleton, Factory or Builder, but there are also others that may be less standardized, one of them is the Container Presenter.

This pattern was introduced a couple of years ago. Originally it was created for React, so for a different environment than Angular. This reason may explain why there is no standardized implementation. I tried to use the Container Presenter pattern myself multiple times. I’ve seen others use it across many Angular applications. I must say it sounds good on paper, but for some reason, in the Angular environment, this pattern doesn’t work.

One explanation for that is that there is no one standard for implementing this pattern. Another reason is that since then Angular evolved. Introduced new features and concepts to the point that the Container Presenter pattern can be used in a different way.

In this article, I will show how you can implement this pattern thanks to the newest features of Angular version 14.

Overview

I guess we all have tried to use this pattern. Some of you more than the others, but for some reason, it doesn’t work. The container presenter was introduced in 2015. It helps to design the architecture of React application in a more separate way. It splits the state layer from the view layer.

Container Presenter chart showing layer separation
Container Presenter chart showing layer separation

In the most basic terms:

  • Container —only managing state; no template,
  • Presenter—only template; no state management.

The issue I see with this concept is that over time components evolve. Developers start to introduce some of a template to the container and some state management to the Presenter. After a while, you cannot say which is which. It’s hard to keep order and force the clean architecture of this pattern.

Issues

  • The Presenter is too smart; injecting services breaks the concept of just focusing on the template,
  • The Container's template gets too complex; allowing the container to have a template leads to putting more and more into it. Instead, there should be only the Presenter,
  • Mixed responsibility, loss of structure, after a while it is hard to say which is which,
  • No separation between two different types of components. Lost of the Single Responsibility Principle.

The better approach

Let's start by defining the requirements for the Container and the Presenter.

Container

  • remove the presentation part from the container and be sure it will not appear,
  • focus on collecting, aggregating, and preparing data so it can be passed to the presentation component.

Presenter

  • focus on the presentation aspect,
  • prevent usage of any code related to state management.

Container implementation

In Angular, there are two main blocks from which applications can be built: components and directives. The main difference between them is that directive doesn’t have a template. This is perfect because it can be used to represent the Container.

Example code of a container directive
Example code of a container directive

As you can see the directive doesn’t have a template, but it has full access to the Dependency injection mechanism. It can take full charge of the container's responsibilities: state management, communication and analytics.

You may wonder, how a directive can communicate with the Presenter if it doesn’t have the template. The answer is by dynamically creating the Presenter component and putting it in the DOM:

Example code of a container directive with ViewContainerRef
Example code of a container directive with ViewContainerRef

The ContainerDirective creates dynamically a PresenterComponent with the help of ViewContainerRef.

Presenter implementation

We have established earlier that The Presenter component should have only one responsibility — It should focus on the template.

Example of a basic PresenterComponent
Example of a basic PresenterComponent

The presenter should avoid using services provided by the Dependency Injection because we want to achieve a clean separation of responsibilities. Services give too much power, taking it away allows us to create a presenter in a way that only focuses on the template.

Communication

Now that we know how the Container and the Presenter look separately, it’s time to make them work together.

The ContainerDirective creates Presenter component dynamically. That means it’s not possible to use inputs and outputs. Instead, we can leverage the fact the Container has a reference to the Presenter instance and can access its properties.

Example of a basic UserPresenterComponent
Example of a basic UserPresenterComponent

The container can access the users property of the presenter object:

Example of a basic UserPresenterComponent with ViewContainerRef
Example of a basic UserPresenterComponent with ViewContainerRef

The concept I have just presented to you is a very basic implementation of what can be achieved. Now as you got the basics I can present you with a more sophisticated solution.

Getting it to work together

Now that you know what are the basics of this concept let’s have a look at the more sophisticated example. Code can be found here:

Container

Let’s start with the Container and see what can be done to improve this pattern. In order to make it simpler and easier to use, we can create a function that will create a Presenter.

@Directive({
   selector: '[user-container]'
})
export class UserContainerDirective {
   private readonly presenter = createPresenter(UserPresenterComponent);
}

In the code example above I have introduced the createPresenter function and this is its implementation:

export function createPresenter<C>(component: Type<C>): C {

	const vcr = inject(ViewContainerRef);

	const compRef = vcr.createComponent(component);

	compRef.changeDetectorRef.detectChanges();

	return compRef.instance;
}

The createPresenter function creates a component and triggers the change detection cycle. It uses the new inject function from Angular14, to get a reference to the ViewContainerRef. After the creation process the createPresenter returns a reference to the created component. This way a container directive can have access to a presenter's properties. It is important for us so we can use it to create a communication channel.

Presenter

In the previous example, we have used a primitive variable for communication. There are obvious issues with that solution so this aspect needs improvements. To resolve this we can define a standardized API that will be used between Container and Presenter. Let’s take a look at the improved code:

@Component({
   template: `
      <ul>
         <li *ngFor="let user of users.value()">
            {{ user }}
         </li>
      </ul>
   `
})
export class UserPresenterComponent {

   users: ReactiveInput<Array<User>> = createInput<Array<User>>();

}

The createInput function brings a more advanced method of communication. It creates an object of type ReactiveInput that allows to:

  • set inputs value,
  • get inputs actual value with the method value,
  • reactively observe changes of the input's value with method connect.
export type ReactiveInput<T> = {
   value: () => T,
   connect: (obs: Observable<T>) => void,
   set: (v: T) => void,
}

The createInput function creates a standardized method of communication. We can use setters and change the value in a nonreactive way or use observables.

export function createInput<T>(): ReactiveInput<T> {

   const cd = inject(ChangeDetectorRef);

   let value: T;

   return {
      set: (v: T) => {
         value = v;
         cd.detectChanges();
      },
      value: () => {
         return value;
      },
      connect: (obs: Observable<T>) => {
         obs.subscribe(v => {
            value = v;
            cd.detectChanges();
         })
      }
   }
}

Now we can adjust the Containers code:

@Directive({
   selector: '[ng-user-container]'
})
export class UserContainerDirective {

   private readonly presenter = createPresenter(UserPresenterComponent);

   constructor(private readonly repo: UserRepository) {

      const users = this.repo.getUsers();

      this.presenter.users.set(users);
   }
}

We can use the reference to the Presenter and set the input value using the previously mentioned ReactiveInput:

this.presenter.users.set(users);

As you can see it is very easy to use. We can do it in the component's constructor, instead of thinking about which life cycle hook should be used. It makes the component's code clearer.

Reactive approach

The shown example doesn’t use a reactive approach to the data flow. It’s natural that UserRepository should return an Observable with an array of users. We can adjust the example:

@Directive({
   selector: '[user-container]'
})
export class UserContainerDirective {

   private readonly presenter = createPresenter(UserPresenterComponent);

   constructor(private readonly repo: UserRepository) {

		const users$ = this.repo.onUsers();

		this.presenter.users.connect(users$);
   }
}

We can use the connect method from the ReactiveInput. It subscribes to the stream and passes the value to the Presenter.

The presented example uses a different method of communication between components other than inputs and outputs. If you are interested in reading more about issues that inputs bring I recommend this article: Watch out for Inputs in Angular

Additional Improvements

There are still some improvements that will make this pattern better:

  • communication from a Presenter to a Container — Outputs,
  • forbid dependency injection in the PresenterComponent,
  • prevent usage of the PresenterComponent outside of the ContainerComponent.

Outputs

In the previous examples, I have shown only the communication to the Presenter. What if a Presenter wants to return some events to its parent Container. We can use the createOutput function:

@Component({
   template: `
	<div (click)="userChanged.emit('click')">Click me</span>
   `
})
export class UserPresenterComponent {

   userChanged = createOutput<User>();

}

The Container component can basically subscribe to the stream of changes:

@Directive({
   selector: '[user-container]'
})
export class UserContainerDirective {

   private readonly presenter = createPresenter(UserPresenterComponent);

   constructor(private readonly repo: UserRepository) {

      this.presenter.userChanged.subscribe(v => this.sendMessage());
   }

   sendMessage() {
   }
}

No Dependency Injection validation

In order to be sure that the presenter takes no part in state management, it should not use dependency injection. This can be achieved by using this one trick that forbids injecting services into a component class.

The presenter component should not inject services
The presenter component should not inject services

As you may see the PresenterComponent is an abstract class that checks the arguments of the constructor. It verifies that an instance of the class was created without passing any parameters to the constructor.

Only use with the Container

The Container and Presenter are a pair of elements that should only work together. You may see an issue with using a Presenter component, not in the context of a Container. The simple solution for that is to remove the selector from the component's metadata. This way you can be assured that it will not be used in a template.

The presenter component should not inject services
The presenter component should not inject services

You may also introduce a naming convention that will suggest to a developer that a component whose selector ends with -presenter should not be used directly in a template of a component.

Benefits

I have shown you a new different approach to the Container Presenter pattern. It offers a lot of benefits over other implementations:

  • Separation of concerns — container is strictly for the state layer, the presenter is for the view layer,
  • Clean API — communication between Container and Presenter is done with the ReactiveInputs which are more advanced than normal Input,
  • Scalability — the container cannot have a template, so it will not be polluted with the view layer; the presenter cannot inject services and take on other responsibilities,
  • The implementation supports TypeScript strict mode,
  • Performance — works well with the ChangeDetectionStrategy.onPush.

The implementation presented in this article works in the applications with an enabled ngZone mechanism. In order to make it work without zones, you need to make some small improvements.

Conclusions

The Container Presenter pattern is an interesting tool to organize the data flow in your Angular application. It can be implemented in different ways, the most important thing is separating layers.

In this article, I have shown you an alternative approach. With it, you can achieve a better separation of responsibilities between the state layer and the view layer. This makes code cleaner and increases the developer's experience.

It's great to use when we want to:

  • explicitly emphasize in the code the separation between state and view layers,
  • split the work between two developers when both of them work on each layer separately.

I think this pattern has lots of benefits and I recommend starting using it in your Angular applications.

Luke, Software engineer, passionate about best practices, frameworks React, Vue, patterns and architectures for modern web
				development, open-source creator, TypeScript enthusiast, Generic UI Technical Blog

Luke

Software engineer, passionate about best practices, frameworks React & Vue, patterns and architectures for modern web development, open-source creator, TypeScript enthusiast.