The Strategy Pattern in Angular: A Comprehensive Guide
Strategy Pattern stands out as a powerful tool for managing algorithms, behaviors, or processes that need to vary independently from clients that use them.
You can read this post or you can watch it live here:
What is the Strategy Pattern?
The Strategy Pattern is a behavioral design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.
In simpler terms, the Strategy Pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. It lets the algorithm vary independently from clients that use it.
Why Do We Need the Strategy Pattern?
Flexibility: it allows you to swap algorithms used inside an object at runtime.
Isolation: it isolates the implementation details of an algorithm from the code that uses it.
Interchangeability: strategies can be interchanged without the client code knowing about it.
Open/Closed Principle: you can introduce new strategies without having to change the context.
Eliminates Conditional Statements: it provides an alternative to using multiple conditional statements in your code.
Implementing the Strategy Pattern in Angular
The code im going to use here it can be found here. And it’s based on this videos:
Let's walk through a practical example of implementing the Strategy Pattern in an Angular application. We'll create a search functionality that can use different search algorithms based on the context
.
The term context
refers to the class that uses a strategy. It's essentially the client that needs to perform some operation, but wants to be flexible about how that operation is carried out. The context is where we "plug in" different strategies.
Think of the context as a socket, and the strategies as different plugs that can fit into that socket. The socket (context) doesn't care which plug (strategy) is inserted, as long as it fits and provides the expected functionality.
Step 1: Define the Strategy Interface
First, we need to define an interface that all concrete strategies will implement:
export interface SearchStrategy {
filter(searchTerm: string): void;
}
Step 2: Implement Concrete Strategies
Now, let's implement some concrete strategies:
// This is an example
@Injectable()
export class PhotoSearchStrategy implements SearchStrategy {
private readonly _appStore = inject(AppStore);
filter(searchTerm: string): void {
if (!searchTerm) {
this._appStore.setPhotosTotals(this._appStore.$photos().length);
this._appStore.setItemsBeingFiltered(0);
this._appStore.setFilteredPhotos([...this._appStore.$photos()]);
return;
}
this._appStore.setFilteredPhotos([...this._appStore.$photos().filter(
(p: Photo) => p.id.includes(searchTerm)
)]);
this._appStore.setItemsBeingFiltered(this._appStore.$filteredPhotos().length);
this._appStore.setPhotosTotals(this._appStore.$photos().length);
}
}
// You can implement others
@Injectable()
export class FavouriteSearchStrategy implements SearchStrategy {
// other logic
}
Step 3: Create a Context
The context
is the class that will use the strategy:
@Injectable()
export class SearchService {
private _strategy!: SearchStrategy;
setStrategy(strategy: SearchStrategy): void {
this._strategy = strategy;
}
filter(searchTerm: string): void {
if (!this._strategy) {
return;
}
this._strategy.filter(searchTerm);
}
}
Every time we will access a route, we will change the strategy, this is awesome!
Step 4: Define resolvers
When we change the route, we must change our strategy with resolvers
(you could use other technique):
export const photoStrategyResolver: ResolveFn<SearchStrategy | null> = () => {
const appStore = inject(AppStore);
appStore.setStrategy(new PhotoSearchStrategy());
return appStore.$strategy();
};
export const favouriteStrategyResolver: ResolveFn<SearchStrategy | null> = () => {
const appStore = inject(AppStore);
appStore.setStrategy(new FavouriteSearchStrategy());
return appStore.$strategy();
};
// routes.ts
{
path: 'photos',
loadComponent: () => import('../modules/components/photos/photos.component').then(
(c) => c.PhotosComponent),
resolve: { strategy: photoStrategyResolver }
},
{
// others...
}
Step 5: Change Strategy depending on Component
Now with the resolver
we can get in our input the strategy and change it in our store.
@Component({
selector: 'app-photos',
templateUrl: './photos.component.html',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PhotosComponent implements OnInit {
readonly appStore = inject(AppStore);
// Strategy from resolver
strategy = input.required<SearchStrategy>();
ngOnInit(): void {
// Change strategy
this.appStore.setStrategy(this.strategy());
}
// ...
}
Step 6: Filter by the correct Strategy on SearchComponent
When you start typing on the search the filter strategy must follow the correct one set by the actual active component:
@Component({
selector: 'app-search',
templateUrl: './search.component.html',
standalone: true,
providers: [SearchService],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchComponent {
private readonly _searchService = inject(SearchService);
private readonly _appStore = inject(AppStore);
private readonly _destroy = inject(DestroyRef);
search = signal('');
searchTerm: Observable<string>;
strategy = computed(() => this._appStore.$strategy());
constructor() {
this._appStore.$clearSearch.pipe(takeUntilDestroyed(this._destroy))
.subscribe(() => this.search.set(''));
// IMPORTANT!:
// This component will remain always active, and we just pick the correct
// strategy each time it changes
this.searchTerm = toObservable(this.search).pipe(
tap(() => this._searchService.setStrategy(
this.strategy() as SearchStrategy)
),
debounceTime(500),
distinctUntilChanged(),
tap((term: string) => this._searchService.filter(term))
);
this.searchTerm.subscribe();
}
}
Advanced Concepts
Using Abstract Classes
Instead of an interface, we could use an abstract class for our strategy:
export abstract class SearchStrategy {
abstract filter(searchTerm: string): void;
protected normalizeString(str: string): string {
return str.toLowerCase().trim();
}
}
This allows us to include some shared functionality (like normalizeString
) that all strategies can use.
I prefer using interfaces and avoid populating the constructor with unnecessary dependencies.
Dependency Injection with InjectionToken
We can use Angular's dependency injection system to provide strategies:
import { InjectionToken } from '@angular/core';
export const SEARCH_STRATEGY = new InjectionToken<SearchStrategy<any>>('SearchStrategy');
@Injectable({
providedIn: 'root',
useFactory: () => new PhotosSearchStrategy()
})
export class DefaultSearchStrategy extends SimpleSearchStrategy<any> {}
@Component({
// ...
providers: [
{ provide: SEARCH_STRATEGY, useClass: DefaultSearchStrategy }
]
})
export class SearchComponent {
constructor(
@Inject(SEARCH_STRATEGY) private strategy: SearchStrategy<any>
) {}
}
This approach allows us to easily switch strategies at the component level.
How to find Strategy Pattern inside Source code of Angular
ChangeDetectionStrategy
The Strategy pattern is implemented here with:
An enum (
ChangeDetectionStrategy
) that defines the available strategies.Usage in component metadata to select the strategy.
Integration with Angular's change detection mechanism.
Key Components
1. ChangeDetectionStrategy Enum
export enum ChangeDetectionStrategy {
OnPush = 0,
Default = 1,
}
This enum defines two strategies:
OnPush
: Change detection is deactivated until explicitly invoked or an input reference changes.Default
: Change detection runs automatically after every event.
2. Usage in Component Decorator
@Component({
// ...
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent { }
Usage Example
@Component({
selector: 'app-performance-critical',
template: `<div>{{ expensiveComputation() }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PerformanceCriticalComponent {
@Input() data: any;
expensiveComputation() {
// This will only be called when `data` reference changes
return heavyProcessing(this.data);
}
}
In this example, the expensive computation will only be re-run when the data
input reference changes, not on every change detection cycle.
Async Pipe
The async pipe
, it's used to handle different types of asynchronous data sources (Observables, Promises, etc.) uniformly.
Key Components
SubscriptionStrategy Interface: this defines the contract for different strategies.
Concrete Strategies: implementations for different types (Observable, Promise).
AsyncPipe Class: the context that uses these strategies.
Implementation Details
1. Transform Method
transform<T>(obj: Observable<T> | Subscribable<T> | Promise<T> | null | undefined): T | null { ... }
This is the main entry point. It handles:
Initial subscription
Resubscription if the source changes
Returning the latest value
2. Strategy Selection
private _selectStrategy(
obj: Subscribable<any> | Promise<any> | EventEmitter<any>,
): SubscriptionStrategy {
if (ɵisPromise(obj)) {
return _promiseStrategy;
}
if (ɵisSubscribable(obj)) {
return _subscribableStrategy;
}
throw invalidPipeArgumentError(AsyncPipe, obj);
}
This method chooses the appropriate strategy based on the type of the input object.
3. Subscription
private _subscribe(obj: Subscribable<any> | Promise<any> | EventEmitter<any>): void {
this._obj = obj;
this._strategy = this._selectStrategy(obj);
this._subscription = this._strategy.createSubscription(obj, (value: Object) =>
this._updateLatestValue(obj, value),
);
}
This method:
Selects the appropriate strategy
Creates a subscription using the selected strategy
Sets up value updating
4. Disposal
private _dispose(): void {
this._strategy!.dispose(this._subscription!);
// Reset state
}
This method cleans up the subscription when needed, using the strategy's dispose method.
TitleStrategy
Angular uses the Strategy pattern to manage page titles during navigation. This implementation allows for flexible and customizable title setting strategies. Let's break down how this works:
An abstract base class (
TitleStrategy
) that defines the interface.A default implementation (
DefaultTitleStrategy
, implied in the code).A custom implementation (
ADevTitleStrategy
).
Key Components
1. Abstract TitleStrategy Class
@Injectable({providedIn: 'root', useFactory: () => inject(DefaultTitleStrategy)})
export abstract class TitleStrategy {
abstract updateTitle(snapshot: RouterStateSnapshot): void;
buildTitle(snapshot: RouterStateSnapshot): string | undefined {
// ... implementation ...
}
getResolvedTitleForRoute(snapshot: ActivatedRouteSnapshot) {
return snapshot.data[RouteTitleKey];
}
}
This abstract class:
Defines the
updateTitle
method that concrete strategies must implement.Provides a default
buildTitle
method that traverses the route tree to find the deepest primary route with a title.Includes a helper method
getResolvedTitleForRoute
to extract the title from route data.
2. Custom ADevTitleStrategy Implementation
@Injectable({providedIn: 'root'})
export class ADevTitleStrategy extends TitleStrategy {
constructor(private readonly title: Title) {
super();
}
override updateTitle(routerState: RouterStateSnapshot) {
const title = this.buildTitle(routerState);
if (title !== undefined) {
this.title.setTitle(title);
}
}
override buildTitle(snapshot: RouterStateSnapshot): string {
// ... custom implementation ...
}
}
This custom strategy:
Overrides
updateTitle
to set the document title using theTitle
service.Provides a custom
buildTitle
implementation that includes additional logic for prefixes and suffixes.
How It Works
Dependency Injection: angular's DI system is used to provide the appropriate strategy. The default is set in the
@Injectable
decorator of the base class.Router Integration: the router uses the injected
TitleStrategy
to update the title after navigation.Customization: developers can create custom strategies (like
ADevTitleStrategy
) and provide them to override the default behavior.
Usage Example
To use a custom strategy:
@NgModule({
// ...
providers: [
{ provide: TitleStrategy, useClass: ADevTitleStrategy }
]
})
export class AppModule { }
This will use the ADevTitleStrategy
for the entire application, overriding the default.
RouteReuseStrategy
Angular employs the Strategy pattern for managing route reuse through the RouteReuseStrategy
class. This implementation allows for flexible and customizable strategies for determining when and how routes should be reused. Let's examine how this works:
An abstract base class (
RouteReuseStrategy
) that defines the interface.A base implementation (
BaseRouteReuseStrategy
) that provides default behavior.A default concrete implementation (
DefaultRouteReuseStrategy
).A custom implementation (
ReuseTutorialsRouteStrategy
).
Key Components
1. Abstract RouteReuseStrategy Class
@Injectable({providedIn: 'root', useFactory: () => inject(DefaultRouteReuseStrategy)})
export abstract class RouteReuseStrategy {
abstract shouldDetach(route: ActivatedRouteSnapshot): boolean;
abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void;
abstract shouldAttach(route: ActivatedRouteSnapshot): boolean;
abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null;
abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
}
This abstract class defines the interface that all route reuse strategies must implement.
2. BaseRouteReuseStrategy Class
export abstract class BaseRouteReuseStrategy implements RouteReuseStrategy {
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return false;
}
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return false;
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
return null;
}
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
}
This base class provides a default implementation that only reuses routes when the matched router configs are identical.
3. DefaultRouteReuseStrategy Class
@Injectable({providedIn: 'root'})
export class DefaultRouteReuseStrategy extends BaseRouteReuseStrategy {}
This is the default strategy used by Angular, which simply extends BaseRouteReuseStrategy
.
4. Custom ReuseTutorialsRouteStrategy Implementation
export class ReuseTutorialsRouteStrategy extends BaseRouteReuseStrategy {
override shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return (
future.routeConfig === curr.routeConfig ||
(this.isTutorialPage(this.getPathFromActivatedRouteSnapshot(future)) &&
this.isTutorialPage(this.getPathFromActivatedRouteSnapshot(curr)))
);
}
// ... helper methods ...
}
This custom strategy extends the base strategy to reuse routes when navigating between tutorial pages.
PreloadingStrategy
The Strategy pattern is implemented here with:
An abstract base class (
PreloadingStrategy
) that defines the interface.Concrete implementations:
PreloadAllModules
: preloads all modules.NoPreloading
: doesn't preload any modules.SelectivePreloadingStrategyService
: a custom strategy for selective preloading.
Key Components
1. Abstract PreloadingStrategy Class
export abstract class PreloadingStrategy {
abstract preload(route: Route, fn: () => Observable<any>): Observable<any>;
}
This abstract class defines the interface that all preloading strategies must implement.
2. PreloadAllModules Strategy
@Injectable({providedIn: 'root'})
export class PreloadAllModules implements PreloadingStrategy {
preload(route: Route, fn: () => Observable<any>): Observable<any> {
return fn().pipe(catchError(() => of(null)));
}
}
This strategy preloads all modules as quickly as possible.
3. NoPreloading Strategy
@Injectable({providedIn: 'root'})
export class NoPreloading implements PreloadingStrategy {
preload(route: Route, fn: () => Observable<any>): Observable<any> {
return of(null);
}
}
This strategy doesn't preload any modules and is enabled by default.
4. Custom SelectivePreloadingStrategyService
@Injectable({
providedIn: 'root',
})
export class SelectivePreloadingStrategyService implements PreloadingStrategy {
preloadedModules: string[] = [];
preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.canMatch === undefined && route.data?.['preload'] && route.path != null) {
this.preloadedModules.push(route.path);
console.log('Preloaded: ' + route.path);
return load();
} else {
return of(null);
}
}
}
This custom strategy selectively preloads routes based on a preload
data property.
Usage Example
To use a custom strategy:
@NgModule({
imports: [RouterModule.forRoot(ROUTES, {preloadingStrategy: SelectivePreloadingStrategyService})],
// ...
})
export class AppModule { }
Or using the standalone API:
bootstrapApplication(AppComponent, {
providers: [
provideRouter(appRoutes, withPreloading(SelectivePreloadingStrategyService))
]
});
How It Works
Dependency Injection: Angular's DI system is used to provide the appropriate strategy. The default is set in the
@Injectable
decorator of the base class.Router Integration: The router uses the injected
RouteReuseStrategy
to determine when to reuse routes during navigation.Customization: Developers can create custom strategies (like
ReuseTutorialsRouteStrategy
) and provide them to override the default behavior.
Usage Example
To use a custom strategy:
@NgModule({
// ...
providers: [
{ provide: RouteReuseStrategy, useClass: ReuseTutorialsRouteStrategy }
]
})
export class AppModule { }
This will use the ReuseTutorialsRouteStrategy
for the entire application, overriding the default.
LocationStrategy
Angular employs the Strategy pattern for managing URL representation through the LocationStrategy
class. This implementation allows for flexible and customizable strategies for representing application state in the browser's URL.
An abstract base class (
LocationStrategy
) that defines the interface.Concrete implementations:
HashLocationStrategy
: Represents state in the hash fragment of the URL.PathLocationStrategy
: Represents state in the path of the URL.
Key Components
1. Abstract LocationStrategy Class
@Injectable({providedIn: 'root', useFactory: () => inject(PathLocationStrategy)})
export abstract class LocationStrategy {
abstract path(includeHash?: boolean): string;
abstract prepareExternalUrl(internal: string): string;
abstract getState(): unknown;
abstract pushState(state: any, title: string, url: string, queryParams: string): void;
abstract replaceState(state: any, title: string, url: string, queryParams: string): void;
abstract forward(): void;
abstract back(): void;
abstract onPopState(fn: LocationChangeListener): void;
abstract getBaseHref(): string;
historyGo?(relativePosition: number): void {
throw new Error(ngDevMode ? 'Not implemented' : '');
}
}
This abstract class defines the interface that all location strategies must implement.
2. HashLocationStrategy
@Injectable()
export class HashLocationStrategy extends LocationStrategy implements OnDestroy {
// ... implementation details ...
override path(includeHash: boolean = false): string {
const path = this._platformLocation.hash ?? '#';
return path.length > 0 ? path.substring(1) : path;
}
override prepareExternalUrl(internal: string): string {
const url = joinWithSlash(this._baseHref, internal);
return url.length > 0 ? '#' + url : url;
}
// ... other method implementations ...
}
This strategy represents the application state in the hash fragment of the URL (e.g.,
http://example.com#/foo
).
3. PathLocationStrategy
@Injectable({providedIn: 'root'})
export class PathLocationStrategy extends LocationStrategy implements OnDestroy {
// ... implementation details ...
override path(includeHash: boolean = false): string {
const pathname =
this._platformLocation.pathname + normalizeQueryParams(this._platformLocation.search);
const hash = this._platformLocation.hash;
return hash && includeHash ? `${pathname}${hash}` : pathname;
}
override prepareExternalUrl(internal: string): string {
return joinWithSlash(this._baseHref, internal);
}
// ... other method implementations ...
}
This strategy represents the application state in the path of the URL (e.g., http://example.com/foo
).
How It Works
Strategy Selection: The location strategy is selected when configuring the application:
providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}]
By default, Angular uses PathLocationStrategy
.
URL Manipulation: The selected strategy is used by the
Location
service to manipulate the browser's URL:
// In Location service
path(includeHash: boolean = false): string {
return this.platformStrategy.path(includeHash);
}
prepareExternalUrl(url: string): string {
return this.platformStrategy.prepareExternalUrl(url);
}
History Management: The strategy also handles browser history management:
pushState(state: any, title: string, url: string, queryParams: string) {
this.platformStrategy.pushState(state, title, url, queryParams);
}
Usage Example
To use the hash location strategy:
@NgModule({
// ...
providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}]
})
export class AppModule { }
When to Use the Strategy Pattern
The Strategy Pattern is particularly useful when:
You have multiple algorithms for a specific task and you want to switch between them at runtime.
You have multiple variants of an algorithm.
An algorithm uses data that clients shouldn't know about.
A class defines many behaviors that appear as multiple conditional statements in its methods.
In Angular applications, common use cases include:
Search algorithms (as in our example)
Sorting strategies
Payment processing methods
Form validation strategies
Data fetching strategies (e.g., from API, local storage, IndexedDB)
Advantages of Using the Strategy Pattern
Cleaner Code: by encapsulating algorithms, your code becomes more organized and easier to understand.
Flexibility: you can easily add new strategies without changing existing code.
Testability: each strategy can be tested in isolation.
Reusability: strategies can be reused across different contexts.
Potential Drawbacks
Increased Number of Classes: each strategy is a separate class, which can increase the overall number of classes in your application.
Client Must Be Aware of Strategies: the client must understand how strategies differ to choose the appropriate one.
Conclusion
The Strategy Pattern is a powerful tool in the Angular developer's toolkit. It promotes clean, flexible, and maintainable code by encapsulating algorithms and making them interchangeable. By understanding and applying this pattern, you can create more robust and adaptable Angular applications.
Remember, like all design patterns, the Strategy Pattern is not a silver bullet. Always consider your specific use case and whether the benefits outweigh the potential drawbacks before implementing it in your project.
Thanks for reading so far 🙏
I’d like to have your feedback so please leave a comment, clap or follow. 👏
Spread the Angular love! 💜
If you really liked it, share it among your community, tech bros and whoever you want! 🚀👥
Don't forget to follow me and stay updated: 📱
Thanks for being part of this Angular journey! 👋😁