Mastering Shared Modules in Angular: Building Reusable and Organized Applications

Angular’s modular architecture is a cornerstone of its ability to create scalable, maintainable, and organized applications. Among the various types of modules in Angular, shared modules play a critical role by centralizing reusable components, directives, pipes, and services that can be used across multiple parts of an application. By encapsulating common functionality, shared modules promote code reuse, reduce duplication, and ensure consistency throughout your project.

In this blog, we’ll dive deep into using shared modules in Angular, exploring their purpose, implementation, and practical applications. We’ll provide detailed explanations, step-by-step examples, and best practices to ensure you can effectively leverage shared modules in your Angular applications. This guide is designed for developers at all levels, from beginners learning Angular’s module system to advanced practitioners architecting large-scale projects. Aligned with Angular’s latest practices as of June 2025 (Angular 17), this content is optimized for clarity, depth, and practical utility.


What Are Shared Modules?

A shared module in Angular is an NgModule that contains reusable components, directives, pipes, or services intended to be imported and used by multiple feature modules or other parts of an application. Unlike feature modules, which encapsulate specific domains (e.g., user management), shared modules focus on providing common utilities that are not tied to a single feature. A shared module is typically imported into other modules, making its exported artifacts available without duplicating code.

Why Use Shared Modules?

Shared modules offer several advantages:

  • Code Reuse: Centralize common components (e.g., buttons, modals) and utilities (e.g., custom pipes) to avoid duplicating code across modules.
  • Consistency: Ensure uniform UI and behavior by using the same components and utilities throughout the application.
  • Maintainability: Update shared functionality in one place, with changes automatically reflected wherever the module is used.
  • Modularity: Keep feature modules focused on their specific domains by offloading common logic to a shared module.
  • Performance Optimization: Reduce bundle size by sharing code, especially when combined with lazy loading.

When to Use Shared Modules?

Use shared modules when:

  • You have components, directives, or pipes used by multiple feature modules (e.g., a custom button or date formatting pipe).
  • You want to share services or utilities across the application without making them singletons.
  • You need to import common Angular modules (e.g., CommonModule, FormsModule) in multiple places.
  • You’re building a large application with multiple teams, where shared utilities ensure consistency.

Avoid using shared modules for:

  • Feature-specific components or services, which belong in feature modules. See [Creating Feature Modules](/angular/modules/create-feature-modules).
  • Singleton services that should be provided app-wide, which belong in a core module.

How Shared Modules Work

A shared module is defined using the @NgModule decorator, which declares components, directives, and pipes, imports dependencies, and exports artifacts for use by other modules. Key characteristics include:

  • Exports: The module exports components, directives, and pipes that other modules can use.
  • Imports: It imports common Angular modules (e.g., CommonModule) or third-party modules (e.g., MatButtonModule) needed by its artifacts.
  • No Routing: Shared modules typically don’t define routes, as they focus on utilities rather than navigation.
  • Multiple Imports: Unlike feature modules, shared modules are designed to be imported by multiple modules without causing conflicts.

Shared modules can be used in both eagerly loaded and lazy-loaded feature modules, making them versatile for various application architectures.


Creating and Using a Shared Module: A Step-by-Step Guide

To demonstrate shared modules, we’ll build a sample Angular application with two feature modules (user management and product catalog) that share common components and a pipe via a shared module. The shared module will include a custom button component, a modal component, and a currency formatting pipe.

Step 1: Set Up the Angular Project

Create a new Angular project if you don’t have one:

ng new shared-module-demo --routing --style=css
cd shared-module-demo
ng serve

The --routing flag generates a routing module, and --style=css sets CSS as the styling format.

Step 2: Generate Feature Modules

Create two feature modules to simulate a real-world application:

ng generate module user --routing
ng generate module product --routing

This creates:

  • user.module.ts and user-routing.module.ts for user management.
  • product.module.ts and product-routing.module.ts for the product catalog.

Step 3: Generate the Shared Module

Create a shared module to hold reusable artifacts:

ng generate module shared

Update shared.module.ts to set up the module:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ],
  exports: []
})
export class SharedModule { }

Explanation:

  • CommonModule: Provides common directives like ngIf and ngFor.
  • declarations and exports: Will include shared components and pipes.

Step 4: Create Shared Components and Pipe

Generate a button component, a modal component, and a currency pipe for the shared module:

ng generate component shared/custom-button --module=shared
ng generate component shared/modal --module=shared
ng generate pipe shared/currency-format --module=shared

The --module=shared flag registers these artifacts in SharedModule.

Custom Button Component

Update custom-button.component.ts:

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-custom-button',
  templateUrl: './custom-button.component.html',
  styleUrls: ['./custom-button.component.css']
})
export class CustomButtonComponent {
  @Input() label: string = 'Click Me';
  @Input() type: 'primary' | 'secondary' = 'primary';
  @Input() disabled: boolean = false;
  @Output() buttonClick = new EventEmitter();

  onClick(): void {
    if (!this.disabled) {
      this.buttonClick.emit();
    }
  }
}

Update custom-button.component.html:

{ { label }}

Update custom-button.component.css:

.custom-button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

.primary {
  background-color: #007bff;
  color: white;
}

.secondary {
  background-color: #6c757d;
  color: white;
}

.custom-button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

Explanation:

  • The button supports customizable label, type, and disabled inputs.
  • It emits a buttonClick event when clicked, respecting the disabled state.
  • Styles are scoped with classes for different types.

Update modal.component.ts:

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-modal',
  templateUrl: './modal.component.html',
  styleUrls: ['./modal.component.css']
})
export class ModalComponent {
  @Input() title: string = '';
  @Input() isOpen: boolean = false;
  @Output() close = new EventEmitter();

  onClose(): void {
    this.close.emit();
  }
}

Update modal.component.html:

{ { title }}
      ×

Update modal.component.css:

.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.modal-content {
  background-color: white;
  border-radius: 8px;
  width: 400px;
  max-width: 90%;
}

.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  border-bottom: 1px solid #ddd;
}

.modal-header h3 {
  margin: 0;
}

.close-btn {
  background: none;
  border: none;
  font-size: 18px;
  cursor: pointer;
}

.modal-body {
  padding: 15px;
}

Explanation:

  • The modal supports title and isOpen inputs to control visibility and content.
  • It emits a close event when the close button is clicked.
  • Uses for content projection, allowing custom content inside the modal.
  • Styles create a centered, fixed-position modal with a backdrop.

For more on content projection, see Using Content Projection.

Currency Format Pipe

Update currency-format.pipe.ts:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'currencyFormat'
})
export class CurrencyFormatPipe implements PipeTransform {
  transform(value: number, currency: string = 'USD'): string {
    if (value == null) return '';
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency
    }).format(value);
  }
}

Explanation:

  • The pipe formats numbers as currency using the Intl.NumberFormat API.
  • Accepts an optional currency parameter (defaults to USD).
  • Handles null/undefined values gracefully.

Step 5: Update the Shared Module

Update shared.module.ts to export the artifacts and include necessary imports:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CustomButtonComponent } from './custom-button/custom-button.component';
import { ModalComponent } from './modal/modal.component';
import { CurrencyFormatPipe } from './currency-format.pipe';

@NgModule({
  declarations: [
    CustomButtonComponent,
    ModalComponent,
    CurrencyFormatPipe
  ],
  imports: [
    CommonModule
  ],
  exports: [
    CustomButtonComponent,
    ModalComponent,
    CurrencyFormatPipe
  ]
})
export class SharedModule { }

Explanation:

  • declarations: Lists the components and pipe.
  • exports: Makes the artifacts available to modules importing SharedModule.
  • CommonModule: Provides directives like *ngIf used in the modal.

Step 6: Implement Feature Modules

User Feature Module

Generate a component for the user feature:

ng generate component user/user-profile --module=user

Update user-profile.component.ts:

import { Component } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html',
  styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent {
  isModalOpen = false;

  openModal(): void {
    this.isModalOpen = true;
  }

  closeModal(): void {
    this.isModalOpen = false;
  }
}

Update user-profile.component.html:

User Profile
  
  
    Edit your profile details here.

Update user-profile.component.css:

.user-profile {
  padding: 20px;
  max-width: 600px;
  margin: 0 auto;
}

h2 {
  text-align: center;
  margin-bottom: 20px;
}

Update user.module.ts to import SharedModule:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserRoutingModule } from './user-routing.module';
import { UserProfileComponent } from './user-profile/user-profile.component';
import { SharedModule } from '../shared/shared.module';

@NgModule({
  declarations: [
    UserProfileComponent
  ],
  imports: [
    CommonModule,
    UserRoutingModule,
    SharedModule
  ]
})
export class UserModule { }

Update user-routing.module.ts:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { UserProfileComponent } from './user-profile/user-profile.component';

const routes: Routes = [
  { path: '', component: UserProfileComponent }
];

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

Explanation:

  • The UserProfileComponent uses the shared CustomButtonComponent and ModalComponent.
  • SharedModule is imported to access shared artifacts.
  • The routing module maps the empty path to the profile component.

Product Feature Module

Generate a component for the product feature:

ng generate component product/product-catalog --module=product

Update product-catalog.component.ts:

import { Component } from '@angular/core';

@Component({
  selector: 'app-product',
  templateUrl: './product-catalog.component.html',
  styleUrls: ['./product-catalog.component.css']
})
export class ProductCatalogComponent {
  products = [
    { name: 'Laptop', price: 999.99 },
    { name: 'Phone', price: 499.99 }
  ];
}

Update product-catalog.component.html:

Product Catalog
  
    
      { { product.name }} - { { product.price | currencyFormat }}

Update product-catalog.component.css:

.product-catalog {
  padding: 20px;
  max-width: 600px;
  margin: 0 auto;
}

h2 {
  text-align: center;
  margin-bottom: 20px;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #ddd;
}

Update product.module.ts:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductRoutingModule } from './product-routing.module';
import { ProductCatalogComponent } from './product-catalog/product-catalog.component';
import { SharedModule } from '../shared/shared.module';

@NgModule({
  declarations: [
    ProductCatalogComponent
  ],
  imports: [
    CommonModule,
    ProductRoutingModule,
    SharedModule
  ]
})
export class ProductModule { }

Update product-routing.module.ts:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProductCatalogComponent } from './product-catalog/product-catalog.component';

const routes: Routes = [
  { path: '', component: ProductCatalogComponent }
];

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

Explanation:

  • The ProductCatalogComponent uses the shared CustomButtonComponent and CurrencyFormatPipe.
  • SharedModule is imported to access shared artifacts.
  • The routing module maps the empty path to the catalog component.

Step 7: Configure Root Routing for Lazy Loading

Update app-routing.module.ts to lazy load the feature modules:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  { path: 'users', loadChildren: () => import('./user/user.module').then(m => m.UserModule) },
  { path: 'products', loadChildren: () => import('./product/product.module').then(m => m.ProductModule) },
  { path: '', redirectTo: '/users', pathMatch: 'full' }
];

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

Explanation:

  • loadChildren: Lazily loads UserModule and ProductModule for their respective routes.
  • The default route redirects to /users.
  • Lazy loading ensures feature modules are loaded only when needed, optimizing performance.

Step 8: Update the Root Template

Update app.component.html to include navigation:

Shared Module Demo
  
    Users
    Products

Update app.component.css:

.app-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

h1 {
  text-align: center;
  margin-bottom: 20px;
}

nav {
  margin-bottom: 20px;
  text-align: center;
}

nav a {
  margin: 0 10px;
  color: #007bff;
  text-decoration: none;
}

nav a:hover {
  text-decoration: underline;
}

Explanation:

  • The navigation links use routerLink to navigate between feature modules.
  • The <router-outlet></router-outlet> renders the active feature module’s components.
  • Basic styling centers the content and styles the navigation.

Step 9: Test the Application

Run the application:

ng serve

Open http://localhost:4200. Test the application by:

  • Navigating to /users to view the user profile, clicking the “Edit Profile” button to open the modal, and closing it.
  • Navigating to /products to view the product catalog, confirming prices are formatted with the CurrencyFormatPipe (e.g., $999.99).
  • Verifying the shared button’s appearance and behavior in both modules.
  • Checking the browser’s Network tab to confirm lazy loading (separate chunks for user.module.js and product.module.js).

This demonstrates the power of shared modules in promoting reuse and consistency.


Advanced Shared Module Scenarios

Shared modules can handle complex requirements. Let’s explore two advanced scenarios to showcase their versatility.

1. Sharing Third-Party Modules

Shared modules can centralize third-party module imports, such as Angular Material, to avoid duplicating imports across feature modules.

Install Angular Material

ng add @angular/material

Choose a theme (e.g., Deep Purple/Amber), enable animations, and set up typography.

Update Shared Module

Update shared.module.ts to include Material modules:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { CustomButtonComponent } from './custom-button/custom-button.component';
import { ModalComponent } from './modal/modal.component';
import { CurrencyFormatPipe } from './currency-format.pipe';

@NgModule({
  declarations: [
    CustomButtonComponent,
    ModalComponent,
    CurrencyFormatPipe
  ],
  imports: [
    CommonModule,
    MatButtonModule
  ],
  exports: [
    CommonModule,
    MatButtonModule,
    CustomButtonComponent,
    ModalComponent,
    CurrencyFormatPipe
  ]
})
export class SharedModule { }

Update User Profile Component

Update user-profile.component.html to use a Material button:

User Profile
  Edit Profile
  
    Edit your profile details here.

Explanation:

  • The SharedModule imports and exports MatButtonModule, making Material’s button available to feature modules.
  • The user profile uses mat-raised-button without directly importing MatButtonModule.
  • Exporting CommonModule ensures common directives are available, reducing boilerplate in feature modules.

For more on Angular Material, see Using Angular Material for UI.

2. Providing Shared Services

Shared modules can provide services for use across feature modules, but care must be taken to avoid multiple instances.

Generate a Shared Service

ng generate service shared/notification

Update notification.service.ts:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root' // Singleton service
})
export class NotificationService {
  notify(message: string): void {
    alert(message); // Replace with a real notification system
  }
}

Use in Feature Modules

Update user-profile.component.ts:

import { Component } from '@angular/core';
import { NotificationService } from '../shared/notification.service';

@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html',
  styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent {
  isModalOpen = false;

  constructor(private notificationService: NotificationService) {}

  openModal(): void {
    this.isModalOpen = true;
  }

  closeModal(): void {
    this.isModalOpen = false;
    this.notificationService.notify('Modal closed');
  }
}

Explanation:

  • The NotificationService is provided at the root level (providedIn: 'root'), ensuring a singleton instance.
  • The user profile uses the service to display a notification when the modal closes.
  • If the service were provided in SharedModule’s providers array, each importing module would create a new instance, which may be undesirable for shared state.

For more on services, see Using Angular Services.


Best Practices for Shared Modules

To use shared modules effectively, follow these best practices: 1. Export Only What’s Needed: Export components, directives, and pipes intended for reuse, keeping internal artifacts private. 2. Avoid Feature-Specific Logic: Keep shared modules free of domain-specific code, reserving that for feature modules. 3. Centralize Common Imports: Export common modules (e.g., CommonModule, FormsModule) to reduce boilerplate in feature modules. 4. Use Singleton Services Carefully: Provide services at the root level or in a core module to avoid multiple instances. See Creating Feature Modules. 5. Optimize for Lazy Loading: Ensure shared modules don’t include heavy dependencies that could impact lazy-loaded feature modules. 6. Test Shared Artifacts: Write unit tests for shared components, pipes, and services to ensure reliability. See Testing Components with Jasmine. 7. Document Usage: Include a README or comments explaining the shared module’s contents and usage. 8. Maintain Versioning: If sharing across projects, version the shared module (e.g., as an npm package) to manage updates.


Debugging Shared Module Issues

If a shared module isn’t working as expected, try these troubleshooting steps:

  • Check Exports: Ensure components, pipes, or modules are exported in SharedModule.
  • Verify Imports: Confirm SharedModule is imported in the consuming feature module.
  • Inspect Service Instances: Log service instances to verify singleton behavior (providedIn: 'root' vs. providers array).
  • Test Lazy Loading: Check the Network tab to ensure shared module artifacts are included in feature module chunks.
  • Review Console Errors: Look for missing dependencies, duplicate declarations, or template errors.
  • Isolate Artifacts: Test shared components or pipes in a standalone context to identify issues.
  • Check Third-Party Modules: Ensure exported third-party modules (e.g., MatButtonModule) are compatible with consuming modules.

FAQ

What’s the difference between a shared module and a feature module?

A shared module contains reusable components, directives, pipes, or modules used across multiple parts of an app, while a feature module encapsulates a specific domain or feature (e.g., user management).

Can a shared module have its own routing?

No, shared modules typically don’t define routes, as routing is feature-specific. Use feature modules for routing logic.

Should I put services in a shared module?

Only if the service is intended for multiple modules and can have multiple instances. For singletons, use providedIn: 'root' or a core module.

Can I use a shared module in lazy-loaded feature modules?

Yes, but ensure the shared module doesn’t include heavy dependencies that could negate lazy loading benefits.


Conclusion

Shared modules are a powerful tool in Angular for promoting code reuse, consistency, and maintainability. By centralizing common components, directives, pipes, and third-party modules, shared modules reduce duplication and streamline development across feature modules. This guide has provided a comprehensive exploration of using shared modules, from creating a module with reusable buttons, modals, and pipes to advanced scenarios like sharing third-party modules and services, complete with practical examples and best practices.

To further enhance your Angular skills, explore related topics like Creating Feature Modules, Using Angular Material for UI, or Creating Custom Pipes.