Angular Training - Modules and standalone components
Angular Training - Modules and standalone components
In the previous article, we saw how to setup an Angular project. In this article, we will see the differences between Modules and Standalone Components. Let’s dive in it!
The repository
We will still use the same repository: Angular Training. This time, we will use the folder 1-modules-and-standalone-components. Again, the exercises are on the main branch while the answers are on the answers branch. Pull the repository and open the project in your favorite IDE.
In your folder, do not forget to execute the npm install
command.
Reminder: this app, called “routines”, is all about setting up your routines.
Our first feature
Counter-intuitively enough, it is considered a bad practice to develop the login page first. A macro-roadmap should go like this: implement public pages -> add security -> implement private pages. Also, it is considered a bad practice to tackle security after having implemented private pages. In our case, we will add the register and login features after developing a first version of our app. Our first feature will be a simple simple list of routines. It will not do much, since we do not have any routine created yet, but it will allow us to see some of the basics.
This page will be our homepage. This information is important for the rest of the training.
The app architecture
A common mistake when we start developing in Angular is to architecture the app by layers: a folder for components, a folder for services, a folder for models, and so on…
Here, we will sort our files by pages and domains. Meaning, if we are about to implement the homepage, we will have a homepage folder right under src
, at the same level as app
.
With a terminal, go in the src
folder and type the following command:
ng g component homepage
This command is a command made available by Angular CLI, that we installed in the previous article. It allows to generate an empty component.
This command will generate four files in a homepage
folder:
homepage.component.html
: the HTML template of our component. This is where we will write the HTML of this page;homepage.component.scss
: the style of our component;homepage.component.ts
: our component per se;homepage.component.spec.ts
: the tests of our component.
The component per se
So far, our component’s code should be something like this:
import { Component } from '@angular/core';
@Component({
selector: 'app-homepage',
imports: [],
templateUrl: './homepage.component.html',
styleUrl: './homepage.component.scss'
})
export class HomepageComponent {
}
The @Component
annotation is called a decorator and is necessary so Angular knows what is a component and what is not.
An object is passed to the decorator:
{
selector: 'app-homepage',
imports: [],
templateUrl: './homepage.component.html',
styleUrl: './homepage.component.scss'
}
selector
: this is the name of the HTML tag we will use to include our component in an HTML template, if necessary;imports
: is used only for standalone components. This property declares all the dependencies our component will rely on;templateUrl
: the relative path to our HTML template;styleUrl
: the relative path to our component’s style. You can specify an array of styles withstyleUrls
(note thes
at the end).
In the previous list, I just said imports
was for standalone components. Standalone components have been a huge revolution for Angular developers, and you know I need to talk about it.
Standalone components vs Modules
Historically, we used to use modules in our Angular apps. It was just the way it was. A module was nothing but a way to encapsulate many components, pipes, services and even other modules under a single category, and expose some or all of them to other modules. Nowadays (since v19), components are standalone by default, meaning they do not need to be declared in a module. A standalone component is self sufficient.
To understand how it worked, use a terminal and go right under the src
folder.
Type the command below:
ng g module shared
This will generate a shared
folder, just like the ng g component homepage
command did, but with a slight difference in it: the folder contains only one file.
shared.module.ts
.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [],
imports: [
CommonModule
]
})
export class SharedModule { }
We find another decorator: @NgModule
, as well as an object in it, with properties such as declarations
and imports
. Here are some specificities about Angular modules I will ask you to duly note and memorize:
- in
declarations
, we declare non-standalone components that will be part of this module, as a non-standalone component cannot be declared in zero modules; imports
works the same way as in our component;- you can add the
providers
property in order to inject services, pipes, and basically everything that is an Injectable (we will cover this later); - you can add the
exports
property, in order to explicit what parts of your module will be exposed for any other modules or components that will use it.
There are a few more properties, less used, that can be pointed out:
bootstrap
: is mostly used for theAppModule
. Specifying a component in it says to Angular: load this component when the app is starting. You must have a corresponding HTML tag in yourindex.html
file. There is no need to specify this property anywhere else than in theAppModule
. But where is myAppModule
, you may ask. Good question: since a few versions of Angular, you no longer need an AppModule thanks tobootstrapApplication()
– although it’s still fully supported for backward compatibility. The bootstraping is done right inmain.ts
with thebootstrapApplication
function.schemas
: rare but usefull. Allows the usse of non-angular elements in your app.
The following properties are deprecated / obsolete / removed:
id
;entryComponents
;jit
;transitiveModule
.
Now, still in our shared
folder, let’s generate a component that will be shared across the entire application. This component will consist of a simple button. We will implement it and style it later.
To do so, use the command we used to generate a component:
ng g component button --standalone=false
Here, we added --standalone=false
: it is necessary to add it if we want to generate a non-standalone component.
So far, here is our structure:
/src
|___
| app // component
| app.component.html
| app.component.scss
| app.component.spec.ts
| app.component.ts
|___
| homepage // standalone component
| homepage.component.html
| homepage.component.scss
| homepage.component.spec.ts
| homepage.component.ts
|___
| shared // module
| button // non-standalone component
| button.component.html
| button.component.scss
| button.component.spec.ts
| button.component.ts
I will now ask you to do the following:
- in your
app.component.html
, replace the content by the following code:
<app-homepage></app-homepage>
<router-outlet />
This code will include our HomepageComponent
in our AppComponent
.
- in
app.component.ts
, addHomeComponent
to the list ofimports
like this:
@Component({
selector: 'app-root',
imports: [RouterOutlet, HomepageComponent], // <= here
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'routines';
}
- in
homepage.component.ts
, import theButtonComponent
as we did forHomepageComponent
in theapp.component.ts
file:
import { Component } from '@angular/core';
import { ButtonComponent } from '../shared/button/button.component';
@Component({
selector: 'app-homepage',
imports: [ButtonComponent], // <= here
templateUrl: './homepage.component.html',
styleUrl: './homepage.component.scss'
})
export class HomepageComponent {
}
- once all of this is done, run
npm run build
again and see many beautiful compilation errors:
$ npm run build
> routines@0.0.0 build
> ng build
Application bundle generation failed. [2.332 seconds]
✘ [ERROR] TS-992011: The component 'ButtonComponent' appears in 'imports', but is not standalone and cannot be imported directly. It must be imported via an NgModule. [plugin angular-compiler]
src/homepage/homepage.component.ts:6:12:
6 │ imports: [ButtonComponent],
╵ ~~~~~~~~~~~~~~~
✘ [ERROR] TS-992010: 'imports' is only valid on a component that is standalone. [plugin angular-compiler]
src/shared/button/button.component.ts:5:11:
5 │ imports: [],
╵ ~~
Did you forget to add 'standalone: true' to this @Component?
src/shared/button/button.component.ts:10:13:
10 │ export class ButtonComponent {
╵ ~~~~~~~~~~~~~~~
The issues are quite simple to understand: ButtonComponent
is not standalone, therefore we should not import it directly in our HomepageComponent
, event if this one is standalone. Instead, we are going to use our SharedModule
.
In order to do so, go into your shared.module.ts
and make sure the ButtonComponent
can be found in the declarations
array.
Once it is done, there is but one little thing to do: add the ButtonComponent
to the exports
array of this module, like so:
@NgModule({
declarations: [
ButtonComponent
],
imports: [
CommonModule
],
exports: [ButtonComponent]
})
export class SharedModule { }
By doing so, we are saying to Angular:”My SharedModule can expose ButtonComponent, so it can be used by consummers of my module”.
And now, in our homepage.component.ts
, we can replace the use of ButtonComponent
in our imports
array by this:
@Component({
selector: 'app-homepage',
imports: [SharedModule],
templateUrl: './homepage.component.html',
styleUrl: './homepage.component.scss'
})
export class HomepageComponent {
}
Now, we can run again npm run build
and… it works!
Conclusion of this article
We started to develop our first feature, and even if we did not develop much, we could see two core concepts of Angular: Modules and Standalone Components.
The first ones are less and less used, as standalone is now the default option. Modules can still be found in legacy projects or libraires, so Angular will keep retro-compatibility for now. They are a bit more verbose than Standalone Components.
The latter are now considered common and best practice. Maybe in a few years, we won’t meet them and Angular will introduce a new type of components, but for now, you should stick to Standalone Components.
What you have learned
- Modules tend to be legacy;
- Modules are just a way to encapsulate a feature, a part of the app, and to expose some resources (components, services, pipes) of that feature to other parts of the app;
- Modules can be imported in Standalone Components;
- Modules can be imported in other Modules;
- Non-standalone Components must belong to at least one module (
declarations
array); - Non-standalone Components cannot be imported directly in Standalone Components and vice-versa;
- Standalone Components can be imported directly in other Standalone Components;
- Standalone components offer a more concise, modular, and flexible structure than traditional modules, especially for modern Angular apps;
- It is still OK to use Modules for now, but the Angular team seems to encourage the use of Standalone Components for better reusability.
🧩 Angular Modules vs Standalone Components
Aspect | Angular Modules (@NgModule ) |
Standalone Components (standalone: true ) |
---|---|---|
Declaration | Components must be declared in declarations |
Components are self-contained with standalone: true |
Importing | Done via imports in a module |
Done directly in other standalone components via imports |
Encapsulation | Groups multiple resources (components, pipes, services…) | Represents an independent functional unit |
Boilerplate | More verbose, requires module boilerplate | More concise and modular |
Interoperability | Required for non-standalone components | Can still import modules like CommonModule , RouterModule |
Recommended Usage | Still useful for grouping legacy or non-standalone components | Recommended as the default since Angular v15, now default in v19 |
Lazy Loading | Uses loadChildren with a module |
Uses loadComponent directly |
Deprecation | Not deprecated yet, but considered legacy going forward | Considered the modern, preferred approach by Angular team |
Migration | Legacy modules can coexist with standalone components | Enables progressive migration from module-based architecture |
Component Exposure | Requires exports to expose components |
No need to export — components are directly importable |
Next lesson
In the next lesson, we will keep on building our feature.