Logo
Published on

Ngmodules vs Standalone Components or Angular2 vs Angular3

Authors
  • Name
    Twitter

Angular, a powerful and popular JavaScript framework, is renowned for its modularity and component-based architecture. At the heart of Angular’s modularity are NgModules and Standalone components, both of which play vital roles in building scalable, maintainable web applications. In this blog📜, we’ll explore the differences between NgModules and standalone components, helping you understand when to use each and how they complement one another.

What are NgModules?

In Angular, a@NgModule is a decorator that represents a cohesive block of code that focuses on a specific functionality or a specific feature of the application. It acts as a container for components, directives, services, and other related entities, providing a clear organization for the application’s codebase. Modules can be lazy loaded when they don’t need to be readily available on startup, which improves the speed of the application.

List of metadata for @NgModule

  • Declarations: In this array, you can add components, directives, and pipes.
  • Imports: other modules whose exported classes and services are needed in this module
  • Exports: The subset of declarations that should be reusable in the component templates of other Ngmodules.
  • Providers: creators of services that this ngModules contributes to the global collection of services.
  • Bootstrap: the main application view (entry point), called a root component, hosts all other application views. (Note):- only the root ngModule should set the bootstrap property.
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'

import { HomeComponent } from './home.component'
import { HostDirective } from './host.directive'
import { customPipe } from './custom.pipe'
import { AppService } from './shared/some.service'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'

@NgModule({
  declarations: [HomeComponent, HostDirective, customPipe],
  imports: [AppRoutingModule, CommonModule],
  providers: [AppService],
  bootstrap: [AppComponent],
})
export class AppModule {}

Here, you can notice that ngModules manages a substantial amount of elements, which consequently results in an expansion of the application’s size and a corresponding reduction in performance.

The confusion starts with components and services not having the same scope(visibility):

  • declarations: components are in local scope (private),
  • providers: services**** are generally in global scope (public).

It means the components you declared are only usable in the current module. If you need to use them outside, in other modules, you’ll have to export them. So here the CoreModules and SharedModules are useful for your app. Read more about ngModules.

What is a Standalone Component? 🌌

Standalone components provide a simplified way to build Angular applications. Standalone components, directives, and pipes aim to streamline the authoring experience by reducing the need for NgModules. Existing applications can optionally and incrementally adopt the new standalone style without any breaking changes.

Angular 14 introduces the standalone component, a component not part of any ngModule that can be used with either other standalone or module-based components.

Besides standalone components, in Angular 14, you can also create:

  • Standalone directives and pipes

You can use a standalone component with:

  • Module-based components
  • Other standalone components
  • Lazy loading and lazy loading routing
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'

@Component({
  selector: 'app-login',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css'],
})
export class LoginComponent {
  constructor() {}
  ngOnInit(): void {}
}

Transform a module-based component into a standalone component by adhering to the following steps:

  1. Set the standalone property to true.
  2. Remove it from the declarationarray of the module of which it was a part.
  3. Use importsarray to add dependencies.

The beauty of standalone components lies in their modular and self-contained nature. Each standalone component is developed independently, with its own set of dependencies and functionality. This isolation allows for better code organization and maintainability, as each component only has access to the dependencies it explicitly requires.

Here are some of the key differences between NgModules and standalone components 🎌:

  • NgModules are used to organize strand manage dependencies📦.

They can be used to group together related components, directives, and pipes and to provide a way to inject dependencies into those components. Standalone components do not require a NgModule to be used. They can be used independently of any other component or NgModule.

  • Standalone components are more flexible than NgModules 💪.

They can be used in any part of an Angular application without being declared in a NgModule. This makes them more reusable and easier to share between different applications. NgModules, on the other hand, can only be used in the application that they are declared in.

  • Standalone components are more lightweight than NgModules 🎈.

They do not require a NgModule to be used, which makes them smaller and faster to load. NgModules, on the other hand, can be larger and slower to load because they need to include all of the dependencies for the components that they are declaring.

Overall, standalone components are a more flexible and lightweight way to build Angular applications. They can be used in any part of an application without being declared in a NgModule, and they are smaller and faster ️to load than NgModules.

So this is the basic difference between ngModules and the standalone component. Let’s now take a closer look at the code base and see how it evolved from version 13 to version 14 and all the way up to the most recent version.


Codebase Structure 📝

  1. With NgModules
import { Component } from '@angular/core'

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  name = 'Angular app'
}
  1. With Standalone Components
import { Component } from '@angular/core';
import { NgIf } from '@angular/common';
@Component({
  selector: 'my-app',
  standalone:true,
  imports:[NgIf]
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  name = 'Angular app';
}

Bootstrapping application 🚀

  1. Using @ngModules, we can bootstrap the application by bootstrapping the AppModule in main.ts and adding AppRoutingModule in AppModule to configure the routing.
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { AppModule } from './app/app.module'

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch((err) => console.error(err))
import { BrowserModule } from '@angular/platform-browser'
import { NgModule } from '@angular/core'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule],
  bootstrap: [AppComponent],
})
export class AppModule {}
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { HomeComponent } from './home/home.component'

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'home', component: HomeComponent },
]

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

Let’s run the app to see if it works! 🤓

Yes, it takes some time to load the app because the size of the bundle💸 is increased.

  1. Using a Standalone approach we can directly bootstrap the AppComponent in main.ts file. and for adding other routes we can simply provide routes with the help of ProvideRoutes().🚀

main.ts

import { bootstrapApplication } from '@angular/platform-browser'
import { AppComponent } from '@app/app.component'
import { provideRouter } from '@angular/router'
import { provideAnimations } from '@angular/platform-browser/animations'
import { appRoutes } from '@routes/app.routes'

bootstrapApplication(AppComponent, {
  providers: [provideRouter(appRoutes), provideAnimations()],
}).catch((err) => console.log(err))

This highlights how the independent API significantly decreases both the bundle size and the amount of code.

Guards and Interceptors 🛡️

  1. Using @NgModulesthe guards are class-based architecture. and it requires more boilerplate code.
import { Injectable } from '@angular/core'
import { CanActivate, Router, UrlTree } from '@angular/router'
import { Observable } from 'rxjs'
import { Constants } from '../../constants/app.constants'
import { EncryptionService } from '../../services/encryption.service'

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(
    private router: Router,
    private _encryption: EncryptionService
  ) {}

  canActivate() {
    if (this._encryption.getDecryptedLocalStorage(Constants.storageKeys.currentUser)) {
      return true
    } else {
      return this.router.parseUrl('/auth/login')
    }
  }
}
  1. Functional guards are introduced in version 14.2. They are a more modern and flexible way to implement guards in Angular.
import { inject } from '@angular/core'
import { CanActivateFn, Router } from '@angular/router'
import { Constants } from '@constants/app.constants'
import { EncryptDecryptService } from '@services/encrypt-decrypt.service'

export const AuthGuard: CanActivateFn = () => {
  const encryptDecryptService = inject(EncryptDecryptService)
  const router = inject(Router)
  const token = encryptDecryptService.getDecryptedLocalStorage(Constants.storageKeys.currentUser)
  if (token) {
    return true
  } else {
    return router.parseUrl('/auth/login')
  }
}

Once you have registered the guard with the router, it will be called whenever a user tries to navigate to the route.

One of the key benefits of functional guards is that they are easy to compose. This means that you can combine multiple functional guards to create more complex guards.

💡How we can miss that the CanMatch guard a best alternative of CanActivate and CanLoad guard.

Interceptor

  1. Using class-based interceptors should implement the HttpInterceptor interface. it requires you to implement intercept() method.

After that, you need to register your custom interceptor in your app’s module. and add it to the providers array in the @NgModule decorator of your module.

import { BrowserModule } from '@angular/platform-browser'
import { NgModule } from '@angular/core'
import { AppRoutingModule } from './app-routing.module'
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'
import { AppComponent } from './app.component'
import { TokenInterceptor } from './core/token-interceptor'

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule, HttpClientModule],
  bootstrap: [AppComponent],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenInterceptor,
      multi: true,
    },
  ],
})
export class AppModule {}

2. Functional interceptors provide a more functional and declarative approach to modifying or inspecting requests and responses compared to class-based interceptors. It uses higher-order functions and can be particularly useful when you want to perform specific tasks on the request or response in a concise and flexible manner.

import { HttpInterceptorFn } from '@angular/common/http'

export const HttpTokenInterceptor: HttpInterceptorFn = (req, next) => {
  req = req.clone({
    setHeaders: {
      Authorization: 'token',
    },
  })
  return next(req)
}

After that, you need to register your custom interceptor in your app config file and add it to the providers array.

import { provideHttpClient, withInterceptors } from '@angular/common/http'
import { ApplicationConfig } from '@angular/core'
import { provideAnimations } from '@angular/platform-browser/animations'
import { provideRouter } from '@angular/router'
import { ErrorInterceptor } from '@interceptor/error.interceptor'
import { HttpTokenInterceptor } from '@interceptor/http.token.interceptor'

import { appRoutes } from '@routes/app.routes'

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(appRoutes),
    provideAnimations(),
    provideHttpClient(withInterceptors([HttpTokenInterceptor, ErrorInterceptor])),
  ],
}

So far, these are the typical features that have been updated from V13 to V14, including the most recent version. Now, let’s delve into server-side rendering and explore how Angular has advanced in the realm of SSR with client hydration.

Angular has supported SSR since Angular Universal, which was introduced in Angular 4. However, SSR in Angular 16 has been significantly improved with the introduction of client hydration.

Here are several benefits to using SSR in Angular, including:

  • Improved performance: SSR can improve the performance of Angular applications by sending the pre-rendered HTML to the client, which can be loaded and displayed more quickly.
  • Improved SEO: SSR can improve the SEO of Angular applications by making it easier for search engines to index the content of the application.
  • Improved user experience: SSR can improve the user experience of Angular applications by displaying the initial content of the application more quickly.

TL;DR let’s see the basic difference between v13 and v14 or the latest version.

In Angular version 13 AppServerModule is bootstrapped in the server configuration file.

import { NgModule } from '@angular/core'
import { ServerModule } from '@angular/platform-server'

import { AppModule } from './app.module'
import { AppComponent } from './app.component'

@NgModule({
  imports: [AppModule, ServerModule],
  bootstrap: [AppComponent],
})
export class AppServerModule {}
import 'zone.js/dist/zone-node'
import * as express from 'express'
import { ngExpressEngine } from '@nguniversal/express-engine'
import { AppServerModule } from './src/main.server'
import { join } from 'path'
import { existsSync } from 'fs'

const app = express()
const distFolder = join(process.cwd(), 'dist/project/browser')
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
  ? 'index.original.html'
  : 'index'
app.engine(
  'html',
  ngExpressEngine({
    bootstrap: AppServerModule,
  })
)

app.set('view engine', 'html')
app.set('views', distFolder)

app.get('*.*', express.static(distFolder))

app.get('*', (req, res) => {
  res.render(indexHtml, { req })
})

app.listen(4000, () => {
  console.log(`Server listening on http://localhost:4000`)
})

Until Angular version 15, Server-Side Rendering (SSR) was imported through the appModule. However, in version 16, a new approach has been introduced. You can enable server-side rendering using provideServerRendering() method.

import { ApplicationConfig, mergeApplicationConfig } from '@angular/core'
import { provideServerRendering } from '@angular/platform-server'
import { appConfig } from '@app/app.config'

const serverConfig: ApplicationConfig = {
  providers: [provideServerRendering()],
}

export const appServerConfig = mergeApplicationConfig(appConfig, serverConfig)
import { enableProdMode } from '@angular/core'
import { bootstrapApplication } from '@angular/platform-browser'
import { AppComponent } from '@app/app.component'
import { appServerConfig } from '@app/app.server.config'
import { environment } from '@environments/environment'

if (environment.production) {
  enableProdMode()
}

const bootstrap = () => bootstrapApplication(AppComponent, appServerConfig)

export default bootstrap
import '@angular/localize/init'
import 'zone.js/node'

import { APP_BASE_HREF } from '@angular/common'
import { ngExpressEngine } from '@nguniversal/express-engine'
import * as compression from 'compression'
import { existsSync } from 'fs'
import { join } from 'path'

import * as express from 'express'
import bootstrap from './src/main.server'

// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
  const server = express()
  server.use(compression())
  const distFolder = join(process.cwd(), 'dist/abctalkies/browser')
  const indexHtml = existsSync(join(distFolder, 'index.original.html'))
    ? 'index.original.html'
    : 'index'

  server.engine(
    'html',
    ngExpressEngine({
      bootstrap,
    })
  )
  server.set('view engine', 'html')
  server.set('views', distFolder)
  server.get('*.*', express.static(distFolder))
  server.get('*', (req, res) => {
    res.render(indexHtml, { req })
  })

  return server
}

function run(): void {
  const port = process.env.PORT || 4200

  // Start up the Node server
  const server = app()
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`)
  })
}

In version 16, Angular introduced Client hydration. It’s a process where the Angular application is re-rendered on the client using the same DOM that was rendered on the server. This allows Angular applications to take advantage of the performance benefits of SSR while still maintaining the interactivity of a client-rendered application.

The purpose of hydration is to improve application performance by reusing existing DOM elements, avoiding extra work to recreate them, and preventing visible UI flickers.

import { provideHttpClient } from '@angular/common/http'
import { ApplicationConfig } from '@angular/core'
import { provideAnimations } from '@angular/platform-browser/animations'
import { provideClientHydration } from '@angular/platform-browser'
import { provideRouter } from '@angular/router'

import { appRoutes } from '@routes/app.routes'

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(appRoutes),
    provideAnimations(),
    provideHttpClient(),
    provideClientHydration(),
  ],
}

You can enable client hydration in your app by incorporating the provideClientHydration() method into the app configuration file.

In addition to these updates, Angular has introduced a range of new features starting from Angular 14. These include Directive composition, Signals, NgOptimized Image, Esbuild dev-server, Dependency injection, Router inputs, and New control flow.

Based on the observed features and numerous modifications in the core files, it appears that Angular 2 has evolved into Angular 3.

Conclusion

In conclusion, the new updated Angular offers a wide range of new features and enhancements that can help to build powerful, efficient, and user-friendly web applications.

Thank you! Happy coding 👨‍💻