Some kittens organizing themselves into formatted modules

Howdy folks! In our last blog post we discussed how to configure an app to lazy load pages. In this post we’ll discuss how to better organize the rest of our app to operate with lazy loading; specifically the UI Components, Directives and Pipes.

Setting the stage

Imagine we have an example music app with these use cases:

  • Lazy loaded: HomePage and DetailPage
  • Two components for rendering our music data
  • Custom pipe to convert milliseconds to minutes:seconds
  • Two directives used in the components

I’ve created this sample app we’ll use as reference in this post, it is available on Github (just switch branches to see the different options we discuss below).

Keep in mind, everyone’s needs are unique, so we recommend reviewing the options presented in this post with your team to determine what works best for your app.

Generally, we recommend following one of two different approaches when developing your app:

  • Option 1 – Encapsulated Modules
  • Option 2 – Shared Common Modules

Each approach has unique benefits and drawbacks, let’s see how they differ in greater detail…

Option 1: Encapsulated Modules

Wrapping all your custom components and pipes into distinct modules (one for components and one for pipes)

With this approach every component, pipe, and directive available to your app can become its own self-contained module. Using the master branch of our sample app, here’s how I structured things:

Within the code of our music-card and music-item components (below), we can see how each of these components exports themselves as encapsulated modules.

// music-card module
import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { MusicCardComponent } from './music-card';
import { MsToMinsPipeModule } from '../../pipes/ms-to-mins/ms-to-mins.module'
@NgModule({
  declarations: [MusicCardComponent],
  imports: [IonicModule, MsToMinsPipeModule],
  exports: [MusicCardComponent]
})
export class MusicCardComponentModule { }
//music-item module
import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { MusicItemComponent } from './music-item';
import { MsToMinsPipeModule } from '../../pipes/ms-to-mins/ms-to-mins.module'
@NgModule({
  declarations: [MusicItemComponent],
  imports: [IonicModule, MsToMinsPipeModule],
  exports: [MusicItemComponent]
})
export class MusicItemComponentModule { }

When we import these modules into their respected pages, we can be certain the compiled chunk will contain the minimum required code it needs to function properly.

Now we can also apply the same concepts to splitting out our directives and pipes. Isolating them into their own modules will ensure that we’re not sending any unnecessary code to our chunks.

import { NgModule } from '@angular/core';
import { LogElmDirective  } from './log-elm';
@NgModule({
  declarations: [LogElmDirective],
  exports: [LogElmDirective]
})
export class LogElmDirectiveModule { }
import { NgModule } from '@angular/core';
import { LogEvntDirective  } from './log-evnt';
@NgModule({
  declarations: [LogEvntDirective],
  exports: [LogEvntDirective]
})
export class LogEvntDirectiveModule { }

The down side of this approach is that there’s more to keep track of as your app grows.
Consider an app with many pages and many components/directives/pipes. As you’re working on things, you’ll need to constantly make sure the correct modules are imported in order to use your custom components. Depending on how many of these modules you need in your page, this could be one import, or 30 imports.

There’s also a bit more boilerplate code for each of these individual modules. While most of that boilerplate code is 6-10 lines, it can be quite repetitive to have constantly go through the ritual of creating a new module every time a new feature is added.

Option 2: Shared Common Modules

Creating a shared module for each component and pipe your app may have.

With this approach, instead of splitting things into their own isolated module, everything gets bundled into a shared components.module.ts The same would go for any pipes we have or any directives we might have as well. A real world example of this would be the angular-moment package from Uri Shaked.

Using the common-mmodules branch of our sample app, here’s how I structured things:

The logic here is that instead of micro-optimizing your app and creating multiple sub-modules, a single shared module contains all the components, pipes. and directives your app needs.
For instance, our music-item and music-card components are not overly complicated components.
Instead of splitting them out, we could package them into a shared components.moudle.ts file:

// components.moudle.ts
// Note that we also need to provide the
// shared PipesModule sice the components use it.

import { NgModule } from '@angular/core';
import { MusicCardComponent } from './music-card/music-card'
import { MusicItemComponent } from './music-item/music-item'
import {IonicModule}  from 'ionic-angular'
import { PipesModule } from '../pipes/pipes.module'
@NgModule({
  declarations: [MusicCardComponent, MusicItemComponent],
  imports: [PipesModule, IonicModule],
  exports: [MusicCardComponent, MusicItemComponent]
})
export class ComponentsModule { }

Doing this means anytime we want to use either music-card or music-item, they’re automatically available in our chunk. The same concept can be applied to directives and pipes.

For our directives:

import { NgModule } from '@angular/core';
import { LogElmDirective } from './log-elm/log-elm'
import { LogEvntDirective } from './log-evnt/log-evnt'

@NgModule({
  declarations: [LogElmDirective, LogEvntDirective],
  exports: [LogElmDirective, LogEvntDirective]
})
export class DirectivesModule {}

Creating shared directives.module.ts and pipes.module.ts will allow you to import these top level modules once, and have all the functionally available.

Of course, this example is not overly complex, but illustrates how developers can apply this pattern.

The down side of the shared common module approach is code is being sent to that chunk regardless if it’s being used. So while we’re using these two components in a single page, any additional components we’re not using still cost us in the price of chunk size. This can be a concern if your app has a lot of components, especially if you’re shipping a web app where everything is sent over the wire. Loading the code for 21-40 components when you only need 4-5 is a big “no-no”.

The key takeaway of these two options for developing components, pipes, and directives for use with lazy loading are:

Option 1
Pro: Easier separation and encapsulation
Con: Additional boilerplate code for each module

Option 2
Pro: Easier to manage multiple components/directives/pipes. They all come from the same shared place.
Con: Extra code is being sent over when it’s not needed. Final chunks can be larger than needed.

So where can you go from here?

There are pros/cons to both of these approaches. Maybe your app is smaller, and wouldn’t benefit from an overly modular approach, then the shared common module solution (option 2, shared common modules) may be of better benefit. Alternatively, your app might contains large quantities of pages, components, directives, and pipes; in this situation you would want to consider making your app more modular (option 1, encapsulated modules).

What we’ve discussed here is only 2 options that we have landed on, but this doesn’t mean you’re limited to just that. You can feel free to experiment and find a balance that fits your team and your app. Whatever approach you land on, the goal will be the same, send as little code as possible per chunk.

And now that we’ve addressed code structure…what else is left?

Since our goal is to send as little code as possible per chunk to the user, and we’ve already split our code into smaller module, the only code left to look at is our libraries. We’ll focus on the some of the most commonly used libraries people include in their apps and some alternatives that are much more mobile friendly.

Links for the sample app:

Signup for the Ionic Newsletter to get the latest news and updates!