Ditch NgModules and embrace the future of Angular development with this complete guide to standalone components
Have you ever found yourself drowning in a sea of NgModules, trying to figure out which component belongs where, or spending precious development time just to set up a simple feature? π€
If you're nodding your head right now, you're not alone. Every Angular developer has been there, and honestly, it's one of those pain points that made many of us question our life choices (okay, maybe just our framework choices).
But here's the thing β Angular 14 introduced something that's quietly revolutionizing how we build applications: Standalone Components. And if you haven't explored them yet, you're missing out on a game-changer.
By the end of this article, you'll understand:
- What standalone components are and why they matter
- Why they're becoming the preferred approach for modern Angular apps
- When to use them (and when maybe not to)
- How to implement them with real-world examples
- The pros and cons you need to know
- Unit testing strategies that actually work
- Bonus tips that'll make you look like a standalone components ninja π₯·
Let's dive in!
What Are Standalone Components? π―
Think of standalone components as self-contained Angular components that don't need to be declared in an NgModule. They're like that friend who shows up to the party with everything they need β no dependencies, no drama, just ready to rock.
Here's a simple example to get us started:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule],
template: `
<div class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<button (click)="onEdit()">Edit User</button>
</div>
`,
styles: [`
.user-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
margin: 8px 0;
}
`]
})
export class UserCardComponent {
user = { name: 'John Doe', email: 'john@example.com' };
onEdit() {
console.log('Edit user:', this.user.name);
}
}
Notice that standalone: true
? That's the magic flag that tells Angular, "Hey, this component can handle itself, thank you very much!"
Quick question for you: How many times have you created a new component and then spent 5 minutes figuring out which module to import it into? Drop a comment below β I bet it's more than you'd like to admit! π¬
Why Standalone Components Are Taking Over π
1. Simplified Mental Model
Remember when you had to think about module hierarchy, imports, exports, and declarations? Standalone components eliminate that cognitive overhead. You import what you need, where you need it.
2. Faster Development
No more module dancing! Create a component, use it. That's it.
3. Better Tree Shaking
Since you're explicitly importing only what you need, your bundle size naturally becomes smaller.
4. Easier Testing
Testing becomes straightforward when your component declares its own dependencies.
Here's a more complex example showing dependency injection:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe, JsonPipe } from '@angular/common';
@Component({
selector: 'app-api-data',
standalone: true,
imports: [AsyncPipe, JsonPipe],
template: `
<div class="api-container">
<h2>API Data</h2>
<pre>{{ data$ | async | json }}</pre>
</div>
`
})
export class ApiDataComponent {
private http = inject(HttpClient);
data$ = this.http.get('http://jsonplaceholder.typicode.com/posts/1');
}
Pretty clean, right? Everything this component needs is right there in the imports array.
When to Use Standalone Components (And When Not To) π€·ββοΈ
β Perfect for:
- New projects (seriously, just start with standalone)
- Shared components across multiple modules
- Lazy-loaded features
- Library components
- Rapid prototyping
β Maybe not ideal for:
- Legacy projects with heavy module coupling (migration can be complex)
- Teams not familiar with the concept (training needed)
- Very large, monolithic applications (though they could benefit the most!)
Real talk: I migrated a mid-sized project last month, and while it took some planning, the development velocity afterward was worth every hour spent on migration.
Have you tried migrating an existing project to standalone components? What was your experience? Let me know in the comments! π
How to Implement Standalone Components: Step-by-Step π
Step 1: Creating Your First Standalone Component
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-todo-item',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="todo-item" [class.completed]="todo.completed">
<input
type="checkbox"
[(ngModel)]="todo.completed"
(change)="onToggle()"
>
<span class="todo-text">{{ todo.text }}</span>
<button (click)="onDelete()" class="delete-btn">ποΈ</button>
</div>
`,
styles: [`
.todo-item {
display: flex;
align-items: center;
padding: 12px;
border-bottom: 1px solid #eee;
}
.completed .todo-text {
text-decoration: line-through;
opacity: 0.6;
}
.delete-btn {
margin-left: auto;
background: none;
border: none;
cursor: pointer;
font-size: 16px;
}
`]
})
export class TodoItemComponent {
todo = { text: 'Learn standalone components', completed: false };
onToggle() {
console.log('Todo toggled:', this.todo);
}
onDelete() {
console.log('Delete todo:', this.todo.text);
}
}
Step 2: Using Standalone Components in Your App
In your main app component:
import { Component } from '@angular/core';
import { TodoItemComponent } from './todo-item.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [TodoItemComponent],
template: `
<div class="app">
<h1>My Todo App</h1>
<app-todo-item></app-todo-item>
</div>
`
})
export class AppComponent {
title = 'standalone-demo';
}
Step 3: Bootstrapping with Standalone Components
Update your main.ts
:
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(),
provideRouter(routes),
// Add other providers as needed
]
}).catch(err => console.error(err));
Pro tip: Notice how clean the bootstrap looks? No more AppModule imports sprawling everywhere!
If you're following along and building this, give it a π β I'd love to know you're actually coding with me!
Pros and Cons: The Real Talk π
β Pros:
Simplified Architecture: No more module puzzles
Better Performance: Improved tree-shaking and lazy loading
Easier Testing: Dependencies are explicit and localized
Faster Development: Less boilerplate, more coding
Future-Proof: Angular is moving in this direction
β Cons:
Learning Curve: New patterns to learn (but worth it!)
Migration Effort: Existing apps need careful planning
Import Management: You'll be managing more imports (but tools help)
Team Alignment: Everyone needs to be on board
Question time: What's been your biggest challenge when adopting new Angular patterns? Drop your thoughts below β let's help each other out! π¬
Unit Testing Standalone Components π§ͺ
Testing standalone components is actually easier than traditional components. Here's how:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TodoItemComponent } from './todo-item.component';
describe('TodoItemComponent', () => {
let component: TodoItemComponent;
let fixture: ComponentFixture<TodoItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TodoItemComponent] // Notice: imports, not declarations!
}).compileComponents();
fixture = TestBed.createComponent(TodoItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should toggle todo completion', () => {
const initialState = component.todo.completed;
component.onToggle();
expect(component.todo.completed).toBe(initialState);
});
it('should render todo text', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.todo-text')?.textContent)
.toContain('Learn standalone components');
});
});
Testing with Dependencies
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ApiDataComponent } from './api-data.component';
describe('ApiDataComponent', () => {
let component: ApiDataComponent;
let fixture: ComponentFixture<ApiDataComponent>;
let httpMock: HttpTestingController;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
ApiDataComponent,
HttpClientTestingModule
]
}).compileComponents();
fixture = TestBed.createComponent(ApiDataComponent);
component = fixture.componentInstance;
httpMock = TestBed.inject(HttpTestingController);
});
it('should fetch data from API', () => {
const mockData = { id: 1, title: "'Test Post' };"
fixture.detectChanges();
const req = httpMock.expectOne('http://jsonplaceholder.typicode.com/posts/1');
expect(req.request.method).toBe('GET');
req.flush(mockData);
component.data$.subscribe(data => {
expect(data).toEqual(mockData);
});
});
afterEach(() => {
httpMock.verify();
});
});
Clean, focused, and easy to understand β just how testing should be!
Bonus Tips That'll Level Up Your Game π
1. Smart Import Organization
Create barrel exports for common imports:
// shared/common-imports.ts
export { CommonModule } from '@angular/common';
export { FormsModule, ReactiveFormsModule } from '@angular/forms';
export { RouterModule } from '@angular/router';
// In your component
import { Component } from '@angular/core';
import { CommonModule, FormsModule } from '../shared/common-imports';
2. Custom Utility Functions
// utils/component.utils.ts
export function createStandaloneComponent(config: {
selector: string;
template: string;
imports?: any[];
}) {
return Component({
selector: config.selector,
standalone: true,
imports: config.imports || [],
template: config.template
});
}
3. Lazy Loading with Standalone Components
// app.routes.ts
export const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
.then(c => c.DashboardComponent)
},
{
path: 'profile',
loadChildren: () => import('./profile/profile.routes')
.then(r => r.PROFILE_ROUTES)
}
];
4. Provider Functions for Services
// services/user.provider.ts
import { Provider } from '@angular/core';
import { UserService } from './user.service';
export function provideUserService(): Provider {
return UserService;
}
// In your component or bootstrap
providers: [provideUserService()]
5. Migration Strategy
When migrating existing apps:
- Start with leaf components (no child components)
- Move shared/utility components next
- Work your way up the component tree
- Keep modules for complex feature areas initially
- Migrate routing last
Quick challenge: Try converting one of your existing components to standalone right now. How long did it take? Comment below with your time β let's see who's the fastest! β±οΈ
The Bottom Line π―
Standalone components aren't just a new feature β they're a paradigm shift that makes Angular development more intuitive, faster, and frankly, more enjoyable. They eliminate the mental overhead of module management while giving you more control over your component dependencies.
The Angular team is clearly betting on this approach for the future, and honestly, after working with them for several months, I can't imagine going back to the old way.
Here's what you should do next:
- Try the examples above in a new project
- Convert one existing component to standalone
- Experiment with the testing patterns
- Share your experience with your team
What Did You Think? π
I'm genuinely curious about your experience with standalone components. Have you tried them yet? What's holding you back if you haven't? Or if you have, what's been your biggest win?
Drop a comment below and let's discuss:
- Which part of this guide was most helpful?
- What challenges are you facing with standalone components?
- Any cool tricks you've discovered that I didn't mention?
Found This Helpful? π
If this article helped clarify standalone components for you, hit that π button! Your claps help other developers discover this content, and honestly, they make my day.
Seriously, if you learned even one new thing, smash that clap button β it takes 2 seconds but means the world to content creators like me.
Want More Tips Like This? π¬
I write about Angular, JavaScript, and frontend development regularly. If you enjoyed this deep dive:
π Follow Me for More Angular & Frontend Goodness:
I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.
- πΌ LinkedIn β Letβs connect professionally
- π₯ Threads β Short-form frontend insights
- π¦ X (Twitter) β Developer banter + code snippets
- π₯ BlueSky β Stay up to date on frontend trends
- π GitHub Projects β Explore code in action
- π Website β Everything in one place
- π Medium Blog β Long-form content and deep-dives
- π¬ Dev Blog β Free Long-form content and deep-dives
Thanks for reading, and happy coding! π
Top comments (0)