Sekrab Garage

Angular Standalone feature

How to turn an Angular app into standalone - Part I

Angular October 20, 22
Series:

Standalone

In this article, we will turn some components in an existing Angular app into standalone components, to see if this feature is worth the trouble. What else do they need to work properly? What scenarios are they best used for? How do they affect development experience? and Output bundles?
  1. one month ago
    How to turn an Angular app into standalone - Part I
  2. one month ago
    How to turn an Angular app into standalone - Part II
  3. one month ago
    Angular standalone router providers: an update
  4. two days ago
    Angular 15 standalone HTTPClient provider: Another update

The claim:

standalone components provide a simplified way to build Angular applications.

Does it? Let’s see.

Mantra

Watching the video in Angular docs standalone comes across as an app-wise architecture. We create standalone components, and route to them. Those components import other standalone components, and the party goes on. The root app component does not need to be standalone obviously, but it is possible.

Angular today provides a transition to the module-less world, we will investigate the following transitions:

  • Turning a common component into a standalone component
  • Turning a pipe and a directive in a library to standalone
  • Grouping multiple shared components in an importable non-modularized group (is it at all possible?)
  • Extracting a feature component to make it standalone and reusable outside its module
  • Creating a standalone routed component (is it worth it?)

Let’s dig. Find the project we are building in StackBlitz

Turning an existing component into standalone

According to the documentation, a stand alone component can still be referenced if it is in the Module, but not under declarations rather imports. So The following example, where we have a toast component in the root component.

The app.module looks like this

// app.module with no standalone
@NgModule({
	declarations: [
		// the app root
		AppComponent,
		// ...
		// our toast non standalone component
		ToastPartialComponent
	],
	imports: [
	  // other needed modules
	  BrowserModule
	  CommonModule,
	 ]
})
export class AppModule { }

And in the app.component itself:

// app.component (root component)
@Component({
  selector: 'app-root',
	// add the gr-toast ToastPartialComponent in root
  template: `<gr-toast></gr-toast>
  <router-outlet></router-outlet>`
})
export class AppComponent {
}

Let’s now adapt the ToastPartialComponent to be standalone, there are two modifications needed along with it. The first, is that the standalone component needs its own imported dependencies, so if we use ngIf we need to import CommonModule. It becomes like this

// lib/toast partial 
@Component({
	selector: 'gr-toast',
	// make it standalone
	standalone: true
	// first, you need to import CommonModule explicitly
	imports: [CommonModule],
	template: `
		 some code that uses ngIf for example
	`
})
export class ToastPartialComponent {
}

The other change is to move the toast component from declarations to imports, like this:

// app.module now imports a standalone component
@NgModule({
	declarations: [
		AppComponent,
	  //...
	],
	imports: [
	  // other needed modules
	  BrowserModule
	  CommonModule,
		// add the standalone component here
		ToastPartialComponent
	 ]
})
export class AppModule { }

According to Angular developers, this is a good place to start transitioning slowly. My take: there is no gain for root components, because we never need to reuse those components. So let’s move on to something less root-y, and more leafy.

Standalone common components, pipes or directives

We shall try three examples in our application. A common partial component, a common pipe, and a common directive.

In a normal application with modules, we usually add our common components to a SharedModule and the pipes and directives into the same module, or in their own LibModule where all reusable library components sit. So, it initially may look something like this:

// An example of a shared module in the current app
@NgModule({
  imports: [
    CommonModule,
    // bring in the LibModule which contains 
	  // the pipes and directives if we need them
    LibModule
  ],
  declarations: [
    // add common components when not standalone
    StarsPartialComponent,
    PagerPartialComponent,
  ],
  exports: [
    StarsPartialComponent,
    PagerPartialComponent,
    CommonModule,
    LibModule,
  ],
})
export class SharedModule {}

Let’s make a star-rating common component and change it into standalone.

Turning a common partial component into standalone

To use the star-rating component today, it should be declared and exported in SharedModule, which is imported into our feature module, in our case it shall be the ProjectRoutingModule.

// routes/project.route: project routing module:
@NgModule({
  imports: [
    // to use a common component, import the shared module
    SharedModule,
    // ...
  ],
  declarations: [ProjectListComponent, ProjectViewComponent],
})
export class ProjectRoutingModule {}

Let’s head to our stars component in the components/common folder, and turn it into a standalone component.

// common/star.partial.ts
@Component({
  selector: 'gr-stars',
  template: `...`
  // turn into a standalone, and now u can import it directly
  standalone: true,
	// ...
})
export class StarsPartialComponent implements OnInit {
 // ...
}

Now we have a couple of options.

  • First option: Remove the stars component from the SharedModule and import it directly into the Project module, or anywhere it needs to be used:
// first option, move the stars out of the shared module
// core/shared.module
@NgModule({
  imports: [
    // ...
  ],
  declarations: [
    // remove from declarations when it is standalone
    // StarsPartialComponent,
    //...
  ],
  exports: [
    // StarsPartialComponent,
    // ...
  ],
})
export class SharedModule {}

Then in the projects routing module, import it

// routes/project.route
@NgModule({
  imports: [
	  // ...
    // import standalone components
    StarsPartialComponent,
  ],
  declarations: [
		// ...
	],
})
export class ProjectRoutingModule {}

// also import it into any module you need it
  • Second option: Move the star component into the imports array of the SharedModule and keep importing the SharedModule as you used to. (We just need to export the common component if we do not need it anywhere else in the SharedModule.)
// second option: move stars to exports of shared module
@NgModule({
  imports: [
    //  ...
    // add standalone only if you intend to reuse within shared component
    StarsPartialComponent,
  ],
  declarations: [
    // remove from declarations when it is standalone
    // StarsPartialComponent,
    // ...
  ],
  exports: [
    // keep exporting standalone
    StarsPartialComponent,
    // ...
  ],
})
export class SharedModule {}
  • Third option: get rid of SharedModule and turn it into an exported const

This third option is what Angular docs provide as an alternative to exporting multiple standalone components without a module. This rather looks like a hack, but it is aligned with the mantra of going module-free in your future apps. It looks like this:

// third option: replace shared module with a const
export const SHARED_COMPONENTS = [
	StarPartialComponent, 
	PagerPartialComponent,
	// ... all standalone shared components
] as const;

Then in our targeted module, like Project module:

// import the const
@NgModule({
  imports: [
    // bring them in
    ...SHARED_COMPONENTS,
  ],
	// ...
})
export class ProjectRoutingModule {}

Next week, we will dive into turning the whole app into module-free, and get rid of the Project module as well.

Importing library pipes and directives into existing modules

This is the biggest win of the standalone feature

Now we are going to get rid of the LibModule and turn the directive and pipe in it into standalone pipes and directives. This is probably the biggest gain since we are used to grouping simple pipes and directives that are used often into a single module, in order to declare them. With standalone we no longer need to declare them. We simply import them whenever we need them. Let’s dig

// in SharedModule stop referencing the LibModule
@NgModule({
 // ...
  exports: [
    // ...
    // standalone, no LibModule any more
    // LibModule,
  ],
})
export class SharedModule {}

In our StackBlitz project I created two components to test with: LetDirective and CustomCurrencyPipe, let’s turn into standalone

// LetDirective
@Directive({
  selector: '[grLet]',
  // turn into standalone
  standalone: true
})
// ...

// CustomCurrencyPipe
@Pipe({ 
  name: 'grCurrency', 
  // turn into standalone
  standalone: true 
})

In our Project module, where we want to use those directives and pipes, we import them. How nice!

// project module now brings in the library items needed
@NgModule({
  imports: [
	  // ...
    // bring in pipes and directives needed
    CustomCurrencyPipe,
    LetDirective
  ],
  // ...
})
export class ProjectRoutingModule {}

We can use the same trick of exported const, but having them individually is better. So from now on, having a group of pipes and directives and common components is no longer a headache. Kudos Angular.

Standalone common feature component

The other very useful scenario is “common feature component.” Take for example our project on StackBlitz. The Project routing module uses a ProjectCardPartialComponent. In a large application where a project card needs to be shown in different modules, in the current module-based Angular architecture, we need to move the card into its own module, and import it whenever needed.

// The project module should contain the card and anything reusable
@NgModule({
  imports: [
    // bring in all modules the card uses
    RouterModule,
    // this one has the Star component
    ...SHARED_COMPONENTS,
    // standalone pipes
    CustomCurrencyPipe,
  ],
  // declare the card
  declarations: [ProjectCardPartialComponent],
  exports: [ProjectCardPartialComponent],
})
export class ProjectModule {}

// and the project route module would import it
const routes: Routes = [
 // ...
];

@NgModule({
  imports: [
    // bring in the project module whenever it needs to be used
    ProjectModule,
    //...
  ],
  // ...
})
export class ProjectRoutingModule {}

Let’s turn the card component into a standalone, and import it into the project route module directly

// project route module
@NgModule({
  imports: [
    // ...
    // the project module no longer needed if the card is standalone
    // ProjectModule,
    // but we need the card standalone component instead
    ProjectCardPartialComponent
  ]
  // ...
})
export class ProjectRoutingModule {}

The project card itself needs a lot of modules to work. Here is how it looks after adaptation:

// ProjectCardPartialComponent 
@Component({
  // ...
  // to turn into standalone
  standalone: true,
  // we need to import everything needed for this component
  imports: [CommonModule, RouterModule, 
  // add StarsPartialComponent, or ...SHARED_COMPONENTS, or SharedModule, all work
  StarsPartialComponent,
  // and bring in the currency pipe
  CustomCurrencyPipe],
})
export class ProjectCardPartialComponent {
  // ...
}

This is also sweet on the long run, it means less stray modules which serve no purpose other than containing reusable feature components.

Standalone routed component

The last scenario for today is a routed component that is fully standalone. Take for example our /content/standalone route. The component uses a pipe, a directive, and some if statement. And let’s spice it up by adding other components, some standalone and others. Here is how the standalone component itself looks like:

@Component({
  templateUrl: './standalone.html',
  // routed standalone component
  standalone: true,
  // needs to import everything it needs, including other standalone components
  imports: [ RouterModule, 
    // lets use the pager component from sharedmodule
    // this also has CommonModule
    SharedModule, 
    // we can do this for the stars component
    ...SHARED_COMPONENTS,
    // and this for the pipe:
    CustomCurrencyPipe,
    // and also this (standalone components only)
    ProjectCardPartialComponent
  ],

})
export class ContentStandaloneComponent  {
  // ...
}

The HTML can have all of the following elements:

<!-- the standalone.html content -->
<!-- CustomCurrencyPipe-->
<p>{{345.25 | grCurrency:'TRY'}}</p>
<!-- ngIf (CommonModule) -->
<div class="box" *ngIf="testMe">
   <h4>Standalone partial common component</h4>
  <gr-stars [rating]="2"></gr-stars>
</div>
<!-- a router link (RouterModule) -->
<a routerLink="/projects">Go to projects</a>
<!-- a non standalone component from SharedModule -->
<gr-pager [isLoadMore]="true"></gr-pager>
<!-- a standalone common feature component -->
<gr-project-card [project]="project"></gr-project-card>

The routing module that includes the standalone route, need not reference anything other than the route, no declarations, no modules:

// content.route
const routes: Routes = [
  //...
  {
    // this is a fully standalone component, it needs no imports in this module
    path: 'standalone',
    component: ContentStandaloneComponent
  }
];

@NgModule({
  imports: [
    // ... no reference to ContentStandaloneComponent
  ],
  declarations: [
    // ... no reference to ContentStandaloneComponent
	],
})
export class ContentRoutingModule {}

Not including any reference to the ContentStandaloneComponent in the routing module, is the biggest win for this case. What we essentially did is move all dependencies from the module, to the component. Which means this component can be sitting anywhere in our app, and routed to from anywhere in the app. This is flexible and I can think of a couple of use cases where this would be really helpful. But on everyday app building, it might not be such a great idea. Modules give us a sense of organization and prevent mix-up between routes.

Conclusion

We went through a quick exercise to change a current angular application into one that uses the standalone feature. Here are my takes thus far:

  • If you are developing for production: WAIT
  • Good candidates for adoption: directives and pipes organized in a library that you don’t use often, or shared components and common feature components that are reusable
  • Not so good places to adopt: routed components, you are better off placing them in a controlled module, root components or components with too many dependencies that are usually passed in from the root module.

Building for SSR and running in browser does not seem to have changed a lot. The bundles have shifted their sizes around. While the main bundle dropped in size, the common lazy loaded chunk increased. Some other chunks increased in size, and others dropped. As for server side lazy chunks, they all seemed to increase in size. On a big application, I can see the drop of the main bundle as a win.

Build standalone result

Fully standalone app

How about a fully standalone app? Given that we are still in development preview, and considering how messy it can get, is it worth it? Let’s dig in. Next week. 😴

  1. one month ago
    How to turn an Angular app into standalone - Part I
  2. one month ago
    How to turn an Angular app into standalone - Part II
  3. one month ago
    Angular standalone router providers: an update
  4. two days ago
    Angular 15 standalone HTTPClient provider: Another update