Angular Feature Toggles (aka Feature Flags) Using LaunchDarkly
📌 Legacy Content Notice: This article was published in October 2022 and uses Angular patterns that were standard at the time (constructor injection,
*ngIfdirective). For modern Angular 17+ applications, consider using:
inject()function instead of constructor injection@ifcontrol flow instead of*ngIfinput()andoutput()functions instead of decoratorsChangeDetectionStrategy.OnPushfor all componentsThe LaunchDarkly integration concepts remain valid; only the Angular syntax has evolved.
Feature toggles (also known as feature flags) are a powerful technique that allows you to enable or disable features in your application without deploying new code. This article demonstrates how to integrate LaunchDarkly's feature flag service with an Angular application using RxJS for reactive updates.
What Are Feature Flags?
Feature flags allow you to:
- Release features gradually to specific user segments
- Test in production safely with beta users
- A/B test different features or implementations
- Kill switches to quickly disable problematic features
- Canary releases to roll out changes incrementally
Prerequisites
- Angular application (v12+)
- LaunchDarkly account (free tier available)
- Basic knowledge of RxJS observables
Step 1: Create a LaunchDarkly Account
- Sign up at https://launchdarkly.com/
- Create a new project
- Get your Client-side ID from the project settings (not the SDK key)
Step 2: Install LaunchDarkly SDK
Install the LaunchDarkly JavaScript client SDK:
npm install launchdarkly-js-client-sdk
Step 3: Create a Feature Flag Service
Create a service to manage LaunchDarkly client and feature flags:
import { Injectable } from '@angular/core';
import * as LDClient from 'launchdarkly-js-client-sdk';
import { Observable, BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class FeatureFlagService {
private ldClient!: LDClient.LDClient;
private flagSubjects = new Map<string, BehaviorSubject<any>>();
constructor() {}
/**
* Initialize LaunchDarkly client
*/
async initialize(clientSideId: string, userContext: LDClient.LDUser): Promise<void> {
this.ldClient = LDClient.initialize(clientSideId, userContext);
await this.ldClient.waitForInitialization();
console.log('LaunchDarkly initialized');
}
/**
* Get a feature flag value as an Observable
*/
getFlag(flagKey: string, defaultValue: any = false): Observable<any> {
if (!this.flagSubjects.has(flagKey)) {
const currentValue = this.ldClient.variation(flagKey, defaultValue);
const subject = new BehaviorSubject<any>(currentValue);
// Listen for flag changes
this.ldClient.on(`change:${flagKey}`, (newValue: any) => {
subject.next(newValue);
});
this.flagSubjects.set(flagKey, subject);
}
return this.flagSubjects.get(flagKey)!.asObservable();
}
/**
* Get all flags
*/
getAllFlags(): LDClient.LDFlagSet {
return this.ldClient.allFlags();
}
/**
* Update user context
*/
async identify(user: LDClient.LDUser): Promise<void> {
await this.ldClient.identify(user);
}
/**
* Close the connection
*/
close(): void {
this.ldClient.close();
}
}
Step 4: Initialize in App Component
Initialize the LaunchDarkly client when your app starts:
import { Component, OnInit } from '@angular/core';
import { FeatureFlagService } from './services/feature-flag.service';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
standalone: true,
template: `<h1>App Root</h1>`
})
export class AppComponent implements OnInit {
constructor(private featureFlagService: FeatureFlagService) {}
async ngOnInit() {
const userContext = {
key: 'user-123', // Unique user identifier
email: 'user@example.com',
name: 'John Doe',
custom: {
role: 'admin',
plan: 'premium'
}
};
await this.featureFlagService.initialize(
environment.launchDarklyClientId,
userContext
);
}
}
Step 5: Use Feature Flags in Components
Now you can use feature flags reactively in your components:
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FeatureFlagService } from './services/feature-flag.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="showNewFeature$ | async">
<h2>New Feature!</h2>
<p>This feature is controlled by LaunchDarkly</p>
</div>
<button [disabled]="!(enableButton$ | async)">
Click me
</button>
`
})
export class DashboardComponent implements OnInit {
showNewFeature$!: Observable<boolean>;
enableButton$!: Observable<boolean>;
constructor(private featureFlagService: FeatureFlagService) {}
ngOnInit() {
this.showNewFeature$ = this.featureFlagService.getFlag('show-new-feature', false);
this.enableButton$ = this.featureFlagService.getFlag('enable-button', true);
}
}
Step 6: Environment Configuration
Add your LaunchDarkly client ID to environment files:
environment.ts:
const environment = {
production: false,
launchDarklyClientId: 'your-client-side-id'
};
environment.prod.ts:
const environmentProd = {
production: true,
launchDarklyClientId: 'your-production-client-side-id'
};
Event Streaming and Real-Time Updates
LaunchDarkly uses WebSocket connections to stream flag changes in real-time. The service automatically listens for changes:
function watchFlagChanges(featureFlagService: FeatureFlagService) {
// Flags update automatically via WebSocket
featureFlagService.getFlag('my-flag').subscribe((value: any) => {
console.log('Flag updated:', value);
// React to flag changes
});
}
User Context and Targeting
LaunchDarkly supports sophisticated user targeting. You can use built-in attributes or custom attributes:
const userContext = {
key: 'user-123', // Required: unique identifier
email: 'user@example.com', // Built-in attribute
name: 'John Doe', // Built-in attribute
country: 'US', // Built-in attribute
custom: { // Custom attributes
role: 'admin',
subscription: 'premium',
accountAge: 365,
betaTester: true
}
};
Advanced: Feature Flag Guards
Create route guards based on feature flags:
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { FeatureFlagService } from './services/feature-flag.service';
import { take, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class FeatureFlagGuard implements CanActivate {
constructor(
private featureFlagService: FeatureFlagService,
private router: Router
) {}
canActivate() {
return this.featureFlagService.getFlag('new-feature-enabled', false)
.pipe(
take(1),
map(enabled => {
if (!enabled) {
this.router.navigate(['/']);
return false;
}
return true;
})
);
}
}
Use in routes:
import { Routes } from '@angular/router';
@Component({ standalone: true, template: '' })
export class NewFeatureComponent {}
const routes: Routes = [
{
path: 'new-feature',
component: NewFeatureComponent,
canActivate: [FeatureFlagGuard]
}
];
Best Practices
- Use descriptive flag names:
enable-dark-modeinstead offlag1 - Always provide default values: Ensure your app works if LaunchDarkly is unavailable
- Clean up old flags: Remove flags and related code after full rollout
- Use targeting rules: Target specific user segments before full rollout
- Monitor flag changes: Track when and how flags are used
Conclusion
LaunchDarkly integration with Angular provides a powerful way to manage feature releases dynamically. By combining LaunchDarkly's real-time updates with RxJS observables, you get a reactive feature flag system that seamlessly integrates with Angular's async pipe and change detection. This approach enables sophisticated release strategies without requiring code deployments.