Picture of chess pawn wearing a crown.

An enterprise approach to the Smart and Dumb components pattern in Angular

When it comes to the Smart and dumb component pattern you can easily find a lot of articles that cover this subject. There is a great one from Dan Abramov dated March 2015! So the concept isn't new. It was originally presented as an architectural design for React applications, but later on, it was applied to all modern frameworks which are built around the component pattern. So it is not surprising that Angular has its own version. Let's take a closer look at this pattern in a more specific environment, so in a big enterprise-scale application.

Enterprise application

Enterprise-scale applications are a strange animal, they require a rigorous approach to programming. What I mean by that is when you work on a project that has a couple of hundred thousand lines of code and there are many teams of developers working simultaneously on one code base, it requires a specific set of rules on issues that do not exist in smaller applications. Most of all, it is very important to write clean, readable code. I highly recommend reading the book "Clean Code" by the great Robert C. Martin.

Image of book 'Clean code' by Robert C. Martin
"Clean Code" by Robert C. Martin

Programmers need to make a greater effort than usual to work together and don't step on each other's feet. The things that may make your application more "clean code" are conventions and patterns. While reading this article please keep in mind that the ideas presented apply mostly to large applications that require an extreme level of cleanliness and order. It doesn't mean you cannot use them in the smaller one. It is a good practice to prepare the architecture of your project for the future.

Image of book 'Clean code' by Robert C. Martin
"Clean Code" by Robert C. Martin

Basics

The smart and dumb components pattern is a very commonly used practice. I like to call it pure and impure, I feel that it makes more sense when you take functional programming into consideration. There are also other names for this pattern: "Container and Presentational", "Advanced and Simple". I personally don't like the term "Smart and dumb" and I try to avoid it as much as possible. On the other hand, it is the most commonly used term, so I will be using it in this article to avoid confusion.

The dumb component is a simple component that has only two responsibilities which are to present given data and to notify its parent about any raw user interaction, such as clicking a button.

The smart component acts as the parent of the dumb one and it handles all data manipulations. So it registers to the store, it maps, filters, reduces and then provides data to the child component. It also retrieves events from user interactions and decides how to modify the state depending on the type of action.

Smart & Dumb components pattern
Smart & Dumb components pattern

The bigger the application the more pairs of small and dumb components you will find. It can be used many times. Many developers are in the habit of simply creating a smart and dumb component whenever data comes from an external service. It is hard to say if this is a good or bad practice, it definitely depends on the situation. The benefits are splitting the application into layers, where less experienced developers work on the presentation components and the more experienced developers on the components responsible for handling the application state. The obvious drawback is overcomplicating the code and putting more unnecessary boilerplate into the repository. I don't want to get into making comparisons between these two types of components, you can read about it in Dan Abramov's article.

Many instances of Smart & Dumb components
Many instances of Smart & Dumb components

Another aspect of bad design in the code is when a dumb component is to smart. As a developer, you always have to be aware of the pros and cons of your solutions. Making a dumb component too smart creates a situation in which you lose all of the benefits of using the smart and dumb pattern in the first place.

There is also no reason why a smart component shouldn't supply data to several dumb components. In most cases, these components occur in a one-to-one relation. One parent component supplies data to one child component. It is also important to state that there is no reason not to use a one-to-many relation, where one smart component supplies data to several dumb components. Everything depends on the situation.

A one-to-many relation
A one-to-many relation

The dumb component

The core responsibility of the dumb component is to present data. It takes data from inputs and emits events with outputs, it cannot do anything else, so no side effects should be generated, like in the case of pure functions. The fact that components are supplied with data by inputs completely agrees with the change detection mechanism. If you want to have a fast and well-performing application, you need to reduce the number of change detection cycles. That means that increasing the number of dumb components will make it faster.

@Component({
  selector: 'user',
  template: `
    Name: {{user.name}}
    Email: {{user.email}}
    <button click="emitActive()">Activate</button>
  `
})
class UserComponent {

  @Input()
  user: User;
  
  @Output()
  activeChanged = new EventEmitter();
  
  emitActive(): void {
    this.activeChanged.emit();
  }
}

The dumb component should be simple, like in the example above. The main focus should be on the template part of the component. The class part should consist only of inputs, outputs and helper methods. Please keep that in mind, because we will try to focus on this assumption in the following part of the article.

Enterprise dumb component

The dumb component should only take values from inputs. They should not use services or any other means of getting data. Basically, if this type of component requires any data, it should get it from the input. So how can we be sure that our component is not getting data from a service? We can forbid the use of any dependency injection!

export abstract class DumbComponent {

   private readonly subClassConstructor: Function;

   protected constructor() {
      this.subClassConstructor = this.constructor;

      if (this.isEmptyConstructor() || arguments.length !== 0) {
         this.throwError('it should not inject services');
      }
   }

   private isEmptyConstructor(): boolean {
      return this.subClassConstructor.toString().split('(')[1][0] !== ')';
   }

   private throwError(reason: string): void {
      throw new Error(`Component "${this.subClassConstructor.name}" is a DumbComponent, ${reason}.`);
   }
}

The isEmptyConstructor checks the number of parameters in the subclass constructor. Basically this method converts constructor function into a string and then using split checks if the function takes any parameters. If the number is greater than zero, that means something has been injected into the component and we want to avoid this from happening.

@Component({...})
export class UserComponent extends DumbComponent {

  constructor(private readonly userService: UserService) {
    super();
  }
  
}

When we try to inject a service into a component derived from a DumbComponent, we get the following runtime error:

Error from the DumbComponent
Error from the DumbComponent

The class part of this type of component should be simple, methods can only act as helpers for template logic. Taking all that into consideration, dumb components should not use an ngOnInit lifecycle hook, because this should only be used to initialize services or subscribe to external sources of data. Dumb components should be stateless. The smart component's only responsibility is to present data, so there is no logical explanation as to why an ngOnInit method should be used. So let's try to prevent it from being used in the DumbComponent.

export abstract class DumbComponent {

   private readonly subClassNgOnInit: Function;

   protected constructor() {
      this.subClassNgOnInit = (this as any).ngOnInit;

      if (this.subClassNgOnInit) {
         this.throwError('it should not use ngOnInit');
      }
   }

   private throwError(reason: string): void {
      throw new Error(`Component "${this.subClassConstructor.name}" is a DumbComponent, ${reason}.`);
   }
}

The implementation is really simple, we basically check if a subclass has a method ngOnInit and if it does, an error is thrown. This implementation of DumbComponent allows us to avoid a situation such as the following:

@Component({...})
export class UserComponent extends DumbComponent {

  users: Array<User>;
  
  constructor(private readonly userService: UserService) {
    super();
  }
  
  ngOnInit() {
    this.userService
        .selectAll()
        .subscribe((users) => {
          this.users = users;
        })
  }
  
}

Clearly we want to forbid selecting data from inside a dumb component. All data handling and manipulation should be done in the smart one.

Other useful lifecycle hooks like ngAfterViewInit are related to the view part of the component. So there is no reason to prevent developers from using them.

Decorator DumbComponent

The dumb component pattern is strongly connected with the change detection mechanism. All dumb components should use the ChangeDetectionStrategy.OnPush, unfortunately, the changeDetection property is a part of the metadata of the @Component decorator and cannot be inherited from the base class. Fortunately, there is another solution, we can create a custom @Component decorator which will always set the desired changeDetection strategy:

const dumbComponentArgs: Component = {
  changeDetection: ChangeDetectionStrategy.OnPush
};

export function DumbComponent(args: Component = {}): (cls: any) => void {

  const compArgs = Object.assign(dumbComponentArgs as Component,args),
    ngCompDecorator = Component(componentArgs);

  return function(compType: any) {
    ngCompDecorator(compType);
  };

}

We can use a custom decorator to ensure that every dumb component uses the OnPush strategy:

@DumbComponent({
  selector: 'user',
  styles: [`...`],
  template: `...`
})
class UserComponent {
}

Unfortunately, this method is not AOT friendly and cannot be used in the production build.

Smart component

The main responsibility of a smart component is getting data from an external source. In most cases, it is done with the help of a service injected into the component. Either it is a simple state service or a more complex service built on the Redux pattern. It returns data as an observable (this approach is strongly connected with the popular NgRx architecture). We always inject a service in the constructor, and then we subscribe to the reactive stream of data in the ngOnInit lifecycle hook.

The component connected to the store
The component connected to the store

A common example of the usage of a smart component can look as follows:

@Component({...})
export class UserListComponent implements ngOnInit, OnDestroy {

  users: Array<User>;
  
  private readonly unsubscribe$ = new Subject();
  
  constructor(private readonly userService: UserService) {
  }
  
  ngOnInit() {
    this.userService
        .selectAll()
        .pipe(takeUntil(this.unsubscribe$))
        .subscribe((users) => {
          this.users = users;
        })
  }
  
  ngOnDestroy() {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }
  
}

It is common practice to build state management services with Rxjs observables. This approach allows us to handle changes in our application reactively. The inconvenience which comes with Rxjs observables is that you always have to remember to unsubscribe. If you don't, you can be sure that sooner or later you will be dealing with a memory leak. This requirement is a really problematic one. If you think you don't always have to unsubscribe, you are wrong, my friend. Give me an example of an observable usage without unsubscribing and I will show you a memory leak.

The reactive approach in the smart component requires subscribing to a service. Every time you create a smart component you also have to implement the part responsible for unsubscribing. It would be nice to have a practice which helps us with that:

export abstract class SmartComponent implements OnDestroy {

   private readonly unsubscribe$ = new Subject<void>();

   private readonly subClassNgOnDestroy: Function;

   constructor() {
      this.subClassNgOnDestroy = this.ngOnDestroy;
      this.ngOnDestroy = () => {
         this.subClassNgOnDestroy();
         this.unsunscribe();
      };
   }

   ngOnDestroy() { }

   protected untilComponentDestroy() {
     return takeUntil(this.unsubscribe$);
   }

   private unsunscribe() {
     if (this.unsubscribe$.isStopped) {
       return;
     }
     this.unsubscribe$.next();
     this.unsubscribe$.complete();
   }
}

The abstract class SmartComponent gives us a handy method takeUntil that helps us deal with unsubscribing. We don't have to remember to implement the ngOnDestroy method either, everything happens automatically. Now our UserListComponent looks like this:

@Component({...})
export class UserListComponent extends SmartComponent implements ngOnInit {

  users: Array<User>;

  constructor(private readonly userService: UserService) {
    super();
  }
  
  ngOnInit() {
    this.userService
        .selectAll()
        .pipe(this.untilComponentDestroy())
        .subscribe((users) => {
          this.users = users;
        });
  }
  
}

The component is cleaner and it's easier to understand because you don't have the code responsible for handling unsubscribing. The reader can focus on what is important in this component and can ignore what is not.

This implementation of the SmartComponent even prevents the programmer from overriding the base ngOnDestroy method:

private readonly subClassNgOnDestroy: Function;

   constructor() {
      this.subClassNgOnDestroy = this.ngOnDestroy;
      this.ngOnDestroy = () => {
         this.subClassNgOnDestroy();
         this.unsunscribe();
      };
   }

This mechanism protects the programmer from overriding the base ngOnDestroy method in the subclass. In the constructor, it assigns an ngOnDestroy method from the subclass to the subClassNgOnDestroy variable and then assigns a new function that invokes both ngOnDestroy methods one after another.

Summary

The Smart & Dumb components is a very popular pattern to use in an angular application. It offers lots of benefits but like every pattern, it may be hard to maintain if you don't agree on a common method of implementation. I hope that conventions presented in this article will help you deal with common issues that may come from using this pattern. Remember that the bigger the application, the stricter the conventions and patterns used should be in order to keep the code growth rate at a stable level.

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

Luke

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

We use cookies to improve your experience. If you continue browsing, we assume that you consent to our use of cookies.