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?
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.
We built the toast service previously in Catching and displaying UI errors with toast messages in Angular
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 theProject
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 theSharedModule
and keep importing theSharedModule
as you used to. (We just need to export the common component if we do not need it anywhere else in theSharedModule
.)
// 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 exportedconst
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.
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. 😴
Top comments (0)