Standardised shipping containers. Photo by chuttersnap on Unsplash.
Original publication date: 2018-11-06.
Through the Model-View-Presenter design pattern, integrating any application state management approach—whether it's a redux-style store like NgRx or straightforward service-based state as seen in the “Tour of Heroes” Angular tutorial—becomes straightforward.
Container components occupy the interface between the presentational layer and the application state. Their roles are twofold:
- They establish a data pipeline for presentation purposes.
- They convert component-specific events into application state commands—or actions, in Redux/NgRx terminology.
Beyond state management, container components can also bridge the UI to other non-presentational layers such as I/O or messaging systems.
This article walks through the process of deriving a container component from a mixed-purpose component.
For background on the terminology used throughout, refer to the introductory piece “Model-View-Presenter with Angular”.
The term container components derives from the fact that these components contain all necessary state for the child components in their view. Furthermore, their view exclusively contains child components—no presentational markup of their own. A container component's template is composed entirely of child components and data bindings.
Another helpful way to conceptualize container components is to view them—much like shipping containers—as fully self-sufficient units that can be freely placed anywhere in component templates, given they expose no input or output properties.
Container components solve the issue of manually forwarding events and properties through multiple levels of the component hierarchy—what the React community calls prop drilling.
We begin with the DashboardComponent from the Tour of Heroes tutorial.
// dashboard.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-dashboard',
styleUrls: ['./dashboard.component.css'],
templateUrl: './dashboard.component.html',
})
export class DashboardComponent implements OnInit {
heroes: Hero[] = [];
constructor(private heroService: HeroService) {}
ngOnInit() {
this.getHeroes();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
}
}
Identify mixed concerns
This component carries responsibilities that span multiple horizontal layers of our application, as detailed in the introductory article.
Horizontal layers of a web application. Open in new tab.
Primarily, it handles presentation—an array of heroes is rendered in its template.
<!-- dashboard.component.html -->
<h3>Top Heroes</h3>
<div class="grid grid-pad">
<a *ngFor="let hero of heroes" class="col-1-4"
routerLink="/detail/{{hero.id}}">
<div class="module hero">
<h4>{{hero.name}}</h4>
</div>
</a>
</div>
<app-hero-search></app-hero-search>
While presentation is a legitimate concern for any UI component, this mixed component is also tightly bound to state management. In an NgRx-based app, it might have injected a Store and retrieved a slice of application state via a selector. In Tour of Heroes, it injects a HeroService, fetches heroes state through an observable, and then filters a portion of the array, storing the result in its heroes property.
Lifecycle hook
It's worth noting that the mixed dashboard component leverages the OnInit lifecycle hook. This is where it subscribes to the observable produced by HeroService#getHeroes. This placement is appropriate, since subscribing initiates a side effect that we prefer to keep out of constructors and property initializers.
Specifically, the act of subscribing to the observable from HeroService#getHeroes triggers an HTTP request. Keeping asynchronous operations out of constructors and property initializers makes components easier to test and reason about.
If you need a refresher on RxJS observable fundamentals, Gerard Sans' article “Angular — Introduction to Reactive Extensions (RxJS)” is a solid resource.
Splitting a mixed component
To separate these multilayer concerns, we divide the mixed component into two distinct parts: a container component and a presentational component.
The container component takes charge of connecting the UI to non-presentational layers—such as application state management and persistence.
Having pinpointed the non-presentational logic, we construct the container component by isolating and relocating that logic—largely through cutting from the mixed component's model and pasting into the container component's model.
// dashboard.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-dashboard',
styleUrls: ['./dashboard.component.css'],
templateUrl: './dashboard.component.html',
})
export class DashboardComponent implements OnInit {
heroes: Hero[] = [];
constructor(private heroService: HeroService) {}
ngOnInit() {
this.getHeroes();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
}
}
// dashboard.component.ts
import { Component } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent {
heroes: Hero[] = [];
}
Once the logic has been moved, transforming the mixed component into a purely presentational one requires a few more steps. These include renaming the tag and aligning its data binding API with what the container template expects—details covered in an upcoming article.
Isolate and extract layer integrations
// dashboard.container.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-dashboard',
templateUrl: './dashboard.container.html',
})
export class DashboardContainerComponent {
topHeroes$: Observable<Hero[]> = this.heroService.getHeroes().pipe(
map(heroes => heroes.slice(1, 5)),
);
constructor(private heroService: HeroService) {}
}
We remove the HeroService dependency and establish a data stream that mirrors the flow within the original mixed dashboard. This is the topHeroes$ observable property, which layers a series of operations atop the observable from HeroService#getHeroes.
This top heroes stream emits only when observed—when a subscription exists. The mapped output is the filtered subset of heroes we intend to display.
Connect the presentational component using data bindings
With the state integration logic extracted, we can—for now—treat the dashboard component as presentational, assuming it will expose a heroes input property as shown in the container's template.
The final step in deriving a container component is linking it to the resulting presentational component through data bindings—property and event bindings within the container's template.
<!-- dashboard.container.html -->
<app-dashboard-ui
[heroes]="topHeroes$ | async"
title="Top Heroes"></app-dashboard-ui>
The app-dashboard-ui tag represents our dashboard component once it becomes presentational. We connect the topHeroes$ observable to its heroes input using the async pipe.
I've also pulled the heading text out of the mixed component, defining it as title in the container's template. The rationale for this will be explained in the upcoming piece on presentational components.
For now, it's enough to note the immediate advantage: the presentational dashboard can be reused elsewhere in the app with a different heading and a different hero subset.
Who manages the subscription?
Interestingly, the ngOnInit lifecycle hook is gone. The container's model prepares the top heroes stream by piping from an existing observable—a process that causes no side effects, meaning no subscription is created.
So where does the subscription happen now? Angular handles it. By using the async pipe in the container's template, we declaratively instruct Angular to subscribe to the top heroes observable.
The outcome is a subscription tied to the presentational component's lifecycle, feeding heroes into the heroes input property.
Removing manual subscription management is welcome, as it's tedious and prone to error. Failing to unsubscribe from an observable that never completes can lead to duplicate subscriptions persisting for the duration of the session, causing memory leaks.
Data flows down from the container component
Figure 1. Data flow starting at a service and ending in the DOM. Open in new tab.
Applying the dashboard feature to Figure 1's flow diagram, we see the container notified of heroes it requested from the service via an observable.
The container computes the top heroes and passes them to the presentational component's input. The hero array could go through a presenter before reaching the DOM, but the container is indifferent to that—it only knows the presentational component's data binding API.
Let's turn to the HeroesComponent from Tour of Heroes for a more intricate case.
// heroes.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-heroes',
styleUrls: ['./heroes.component.css'],
templateUrl: './heroes.component.html',
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
constructor(private heroService: HeroService) {}
ngOnInit() {
this.getHeroes();
}
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.addHero({ name } as Hero)
.subscribe(hero => {
this.heroes.push(hero);
});
}
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
this.heroService.deleteHero(hero).subscribe();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
}
Isolate layer integrations
At first glance, this component may appear small and unassuming. Up close, though, it's packed with concerns (pun intended). Like the earlier example, the ngOnInit lifecycle hook and the getHeroes method deal with querying application state.
Horizontal layers—or system concerns—of a web application. Open in new tab.
The delete method manages persistent state by replacing the heroes property with an array that omits the removed hero. It also touches persistence, deleting the hero from server state via the hero service.
Finally, the add method handles user interaction, validating the hero name before creating a hero—an action that concerns both persistence and application state layers.
Isolating layer-specific concerns
Time to roll up our sleeves! We'll strip out those cross-cutting system concerns by moving them into a container component.
// heroes.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.container.html',
})
export class HeroesContainerComponent implements OnInit {
heroes: Hero[];
constructor(private heroService: HeroService) {}
ngOnInit() {
this.getHeroes();
}
add(name: string): void {
this.heroService.addHero({ name } as Hero)
.subscribe(hero => {
this.heroes.push(hero);
});
}
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
this.heroService.deleteHero(hero).subscribe();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
}
Following the pattern from the basic example, we move the HeroService dependency into a container component. The heroes state lives in the mutable heroes property.
That approach works fine with the default change detection strategy, but we're aiming for better performance with OnPush. To make that work, we need an observable to hold the heroes state.
The hero service hands back an observable that emits an array of heroes, yet we also need to handle adding and removing heroes. A straightforward way is to build a stateful observable using a BehaviorSubject.
But there's a catch: using a subject means subscribing to the hero service observable, which introduces a side effect. If that observable keeps emitting beyond a single value and never completes, we'd have to track and clean up the subscription ourselves to avoid memory leaks.
On top of that, we'd need to reduce the heroes state for each addition or removal. That complexity starts to snowball quickly.
Handling state reactively
To manage application state in a reactive manner, I put together a small library called rxjs-multi-scan. The multiScan combination operator merges several observables through a single scan to compute the current state, with a—usually compact—reducer function for each observable source. The operator takes the initial state as its final argument.
Each odd parameter—except for the initial state—is a source observable, and the immediately following even parameter is the reducer function that processes the scanned state.
// heroes.container.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { noop, Observable, Subject } from 'rxjs';
import { multiScan } from 'rxjs-multi-scan';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-heroes',
templateUrl: './heroes.container.html',
})
export class HeroesContainerComponent {
private heroAdd: Subject<Hero> = new Subject();
private heroRemove: Subject<Hero> = new Subject();
heroes$: Observable<Hero[]> = multiScan(
this.heroService.getHeroes(),
(heroes, loadedHeroes) => [...heroes, ...loadedHeroes],
this.heroAdd,
(heroes, hero) => [...heroes, hero],
this.heroRemove,
(heroes, hero) => heroes.filter(h => h !== hero),
[]);
constructor(private heroService: HeroService) {}
add(name: string): void {
this.heroService.addHero({ name } as Hero)
.subscribe({
next: h => this.heroAdd.next(h),
error: noop,
});
}
delete(hero: Hero): void {
this.heroRemove.next(hero);
this.heroService.deleteHero(hero)
.subscribe({
error: () => this.heroAdd.next(hero),
});
}
}
In our scenario, the initial state is an empty array. When the observable from HeroService#getHeroes emits an array of heroes, they get concatenated onto the current state.
I set up an RxJS Subject for each user action—one for adding a hero, another for removing one. When a hero passes through the private heroAdd property, the associated reducer inside multiScan appends it to the current state.
For removal, a hero emitted via the heroRemove subject triggers a filter on the current heroes state, dropping the specified hero from the list.
Strategies for updating persistence
We expose the ability to add or delete a hero through the public methods add and delete. For adding, we follow the pessimistic update strategy: first persist the hero to the server via the hero service, and only upon success do we update the persistent state in heroes$.
Currently, errors during server updates aren't handled—you can see the error handler in the subscribe observer is set to noop. If we wanted to show a toast or retry the operation, that error handler is where we'd put that logic.
When deleting, we use the optimistic update strategy: remove the hero from the persistent state right away, then delete it from the server. If the server deletion fails, we roll back the persistent state by pushing the hero back into heroes$ via the heroAdd subject.
That's a step up from the original code, which didn't account for server errors on deletion at all.
Events bubble up to the container
Figure 2. Event flow starting with a user interaction and ending in a service. Open in new tab.
Let's walk through how the heroes feature fits into the flow diagram from Figure 2. Picture the user typing a hero name and pressing the Add button.
A method on the presentational component's model gets called with the new hero's name. The presentational component might offload user interaction handling to a presenter before emitting the hero name as an event through one of its output properties.
The container component picks up that emitted hero name, forwards it to the hero service, and then refreshes the persistent state stored in the container component's model.
The updated heroes state flows back to the presentational component, and the data cycle continues as shown in Figure 1.
Application state stands apart
It's worth clarifying that although application state can be tied to a specific feature, the heroes state appears across different parts of Tour of Heroes. As noted earlier, it's persistent state that mirrors a piece of the server state. Ideally, our heroes container component wouldn't manage persistent state on its own—it would lean on the hero service for that, or the store in a setup using NgRx Store.
Even though the heroes state is handled in a feature-specific container component, it stays consistent across the app. That's because the dashboard fetches the heroes server state from the hero service each time it initializes, triggering an HTTP request that hydrates (seeds) the persistent state.
In these companion articles, our focus is on Angular components. To keep that focus, we won't be touching services. If you want the heroes state housed in the hero service where it logically belongs, you can pull the state management out of this container component.
See how simple it becomes to isolate specific logic once concerns are separated? You can slot it into whichever application layer suits it best.
Handling immutable data
In the mixed heroes component, Array#push was used to add a hero to the state. That mutates the array, so no new reference is produced. While that's acceptable with Angular's default change detection, we're choosing performance with OnPush across all our components.
For OnPush to function correctly, we need a fresh array reference each time a hero is added. We accomplish this by using the spread operator (...) inside a new array literal to copy over the current heroes from the snapshot value and include the new one. That new array gets emitted to subscribers of the heroes$ property.
What stays behind
If you're coding along, you might notice we kept the validation logic in the mixed heroes component. That's deliberate—it's not tied to application state or persistence.
// heroes.component.ts
import { Component } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent {
heroes: Hero[];
add(name: string): void {
name = name.trim();
if (!name) { return; }
}
delete(hero: Hero): void {}
}
Wired up through the data binding API
The last step is linking the container component to the presentational component's data binding API within the container component's template.
<!-- heroes.container.html -->
<app-heroes-ui
[heroes]="heroes$ | async"
title="My Heroes"
(add)="add($event)"
(remove)="delete($event)"></app-heroes-ui>
As in the simpler example, we bind the heroes input property to our observable by piping it through async. That delivers a new array reference to the presentational component every time the heroes state shifts.
Keep in mind that when we leverage the async pipe, Angular handles the subscription to the heroes$ observable, tying it to the presentational component's lifecycle.
Event bindings
In the presentational heroes component, users can alter the application state by adding or removing heroes. We expect the presentational component to emit a hero via an output property whenever the user adds or removes one, so we wire the add method of the container component to the presentational component's add event.
Similarly, we attach the delete method to the remove event. I chose the name delete because the goal is to remove the hero from the server state while keeping the persistent state aligned.
Deletion is an intent a container component is expected to handle, whereas a presentational component should steer clear of application state except for local UI state. It can only fire a component-specific event when the user wants a hero removed. The remove event is then translated into a persistence command by the heroes container component, which is expected to modify the application state. The resulting new state cascades down to the presentational component's input properties as a fresh array reference.
Enabling OnPush change detection
For a container component to function optimally, the application state should flow through observables. Simultaneously, all data within those observables must be treated as immutable structures.
This setup makes it possible to adopt the OnPush change detection strategy in the container component. The async pipe is responsible for triggering change detection whenever a new value is emitted. Since immutable data structures guarantee a fresh reference for every emission, the same OnPush strategy can also be applied to the presentational components that receive this data.
Our initial HeroesComponent consisted of four associated files:
- The dedicated stylesheet
- The HTML template
- The unit test suite
- The data model
heroes
├── heroes.component.css
├── heroes.component.html
├── heroes.component.spec.ts
├── heroes.component.ts
├── heroes.container.html
├── heroes.container.spec.ts
└── heroes.container.ts
Heroes: layout of files for the container component.
We then added the HeroesContainerComponent along with its test suite. A container component typically does not require its own stylesheet, which means only three new files were introduced.
The chosen approach was to house all files in one directory. The naming convention for the container component mirrors that of the mixed component, but uses a .container suffix instead of .component.
It's worth stressing that the naming of files, folders, and classes is entirely up to you. This is a guideline, not a rigid rulebook.
Do you lean towards inline templates and styles? Or would you rather separate the mixed and container components into different directories? Feel free to adopt whatever suits your workflow and your team best.
Here's a recap of the process for turning a mixed component into a container component:
- Separate the logic that interfaces with non-presentational layers into a dedicated container component.
- Have the container component expose application state via observables.
- Bind the container component's data to the presentational component.
- Switch to the
OnPushchange detection strategy.
Keep in mind the dual role of container components:
- They provide a stream of data for presentation.
- They convert component-specific events into commands for the application state—or actions, to use Redux/NgRx Store terminology.
A key advantage of this pattern is the boost in testability. Dive deeper into this with “Testing Angular container components”.
Start with the foundational post “Model-View-Presenter with Angular”.
That article also contains links to the companion GitHub repository, further reading, and other handy resources.
For insight into testing container component logic with rapid unit tests, see “Testing Angular container components”.
Once the container component has been extracted from the mixed component, the next step is to reshape the remaining parts into a presentational component. That topic is addressed in "Presentational components with Angular".
The concept of container components has been a staple in the React ecosystem for quite some time.
The earliest known introduction of the idea appears in the talk “Making Your App Fast with High-Performance Components” delivered by Jason Bonta during React Conf 2015:
Making Your App Fast with High-Performance Components, React Conf 2015. Open in new tab.
In a 2015 article titled “Container Components”, Michael “chantastic” Chan expands on this and provides a sample component for reference.
Dan Abramov outlines his own methodology for splitting React components into container and presentational categories in his 2015 piece “Presentational and Container Components”. He also explores linked ideas such as stateful and stateless components.
Editorial support
I am grateful to you, Max Koretskyi, for your assistance in polishing this article. Your willingness to share your insights on writing for the developer community is highly valued.
Review board
My sincere thanks go out to all the reviewers who helped bring this article to life. Your contributions were immensely helpful!


