How to Rewrite Angular Apps to be Nearly Observable and Subscribe-Free

Doguhan Uluca
4 min readJan 21, 2024

Managing subscriptions is one of the trickiest parts of working with RxJS and Observables in Angular. Even with helpers like the async pipe, takeUntilDestroyed, and auto-unsubscribe, it’s easy to run into bugs and memory leaks. The new Signals feature in Angular aims to solve this problem by introducing a simpler, subscription-less reactive programming model.

Combined with the powerful new NgRx SignalStore state management library, we can rewrite Angular apps to be nearly Observable and subscribe-free. A prime example of this can be found in my book, Angular for Enterprise Applications, specifically in Chapter 9, Recipes — Master/Detail, Data Tables, and NgRx. This chapter delves into the innovative use of NgRx/SignalStore and SignalState to transform Angular applications, reducing complexity while enhancing performance.

Simplifying with NgRx/SignalStore

The concept of using NgRx/SignalStore is inspired by the need to minimize the use of Observables, which often leads to complex and error-prone code. As the book shows through the example of the LocalCast Weather app, rewriting an application with NgRx/SignalStore results in a simpler, more concise codebase.

LocalCast Weather App

Benefits of SignalStore

SignalStore stands out for its inherent simplicity compared to traditional NgRx store implementations. It integrates seamlessly with Angular’s reactivity system, eliminating the need for explicit subscribe calls or async pipes. This is vividly demonstrated in the book’s section on Refactoring RxJS and NgRx code, where the LocalCast Weather app undergoes a transformation using SignalStore​​.

You can find the source code for this example at https://github.com/duluca/local-weather-app. The root folder reflects the initial state of the code, and the projects/signal-store folder is the end state.

Let’s see a quick overview of key elements that go into refactoring RxJS in favor of signals.

Replacing Observables with Promises and Signals

The first key is to replace Observable HTTP responses and BehaviorSubject data anchors with Promise-based APIs instead. NgRx SignalStore uses signals under the hood, which manage their subscriptions. We can bridge remaining Observables with helpers like:

  • toSignal: Converts an Observable to a signal.
  • lastValueFrom: Converts an Observable to a Promise.

We must also set the change detection strategy to OnPush so components only update when their inputs change.

Refactoring the Weather Store

A major step in this transformation is refactoring the NgRx Store using SignalStore. The WeatherStore is defined as follows:

export const WeatherStore = signalStore(
{ providedIn: 'root' },
withState({ current: defaultWeather }),
withMethods((store, weatherService = inject(WeatherService)) => ({
async updateWeather(searchText: string, country?: string) {
patchState(store, {
current: await weatherService.getCurrentWeather(searchText, country)
})
}
}))
)

This approach simplifies state management by leveraging the inherent reactivity of signals within Angular​​ while bringing reducers, actions, and effects into one place.

Updating the City Search Component

In the CitySearchComponent, we use a signal to handle search value changes. This is demonstrated in the component's implementation:

export class CitySearchComponent {
private readonly store = inject(WeatherStore);
search = new FormControl('', [Validators.required, Validators.minLength(2)])
readonly searchSignal = toSignal(
this.search.valueChanges.pipe(
filter(() => this.search.valid),
debounceTime(1000)
)
);

constructor() {
effect(() => {
this.doSearch(this.searchSignal())
})
}

doSearch(searchValue?: string | null) {
if (typeof searchValue !== 'string') return;
const [searchText, country] = searchValue.split(',').map(s => s.trim())
this.store.updateWeather(searchText, country)
}
}

Here, the toSignal function converts the valueChanges observable of the search control into a signal, which is then used to trigger the doSearch method​​.

Filter and debounce are the only RxJS operators left in LocalCast Weather. Currently, there aren’t operators for signals. It’s better to use a well-tested library for now.

CurrentWeather Component

In the CurrentWeatherComponent, the change detection strategy is set to OnPush, and WeatherStore is injected:

@Component({
selector: 'app-current-weather',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CurrentWeatherComponent {
readonly store = inject(WeatherStore);

// Template binding example
// <div>{{ store.current().city }}</div>
}

In this component, the template is directly bound to the store.current() signal, simplifying the data flow and reducing the need for additional RxJS operators​​.

NgRx/SignalState: A Lightweight Alternative

For managing simpler states within components or services, NgRx/SignalState is a valuable tool. It allows for direct operation on small slices of state, maintaining a minimalist and efficient approach. This utility is particularly effective for tracking API calls within an application without needing a local state in a service, as discussed in the book​​.

Why Choose This Approach?

Adopting NgRx/SignalStore and SignalState in Angular applications offers several advantages:

  • Reduced Complexity: By eliminating unnecessary Observables and RxJS operators, the code becomes easier to understand and maintain.
  • Enhanced Performance: These tools leverage Angular’s reactivity system for more efficient state management.
  • Simpler State Management: SignalStore and SignalState provide a straightforward way to manage complex and simple states, respectively.

Future of Angular with Signals

Looking ahead, I anticipate a future where signal-based components will further streamline Angular development. This approach, which could be similar to the async pipe’s functionality, promises to reduce the need for template rewrites and simplify the overall code structure​​.

Conclusion

This blog post covers just a glimpse of the innovative approaches detailed in Chapter 9 of Angular for Enterprise Applications. The chapter delves into various Angular app design considerations, such as router-first architecture and implementing a line-of-business app. Readers interested in these topics are encouraged to explore the book further.

Angular for Enterprise Applications, Third Edition

Get Your Copy Today: The book, updated with Angular 17.1, was released on January 31, 2024. It’s an invaluable resource for any developer looking to master Angular for enterprise applications. Visit AngularForEnterprise.com to purchase your copy and dive deeper into the world of Angular development.

--

--

Doguhan Uluca

Technologist, Author, Speaker. Agile, JavaScript and Cloud expert. Angular GDE Alumni. Go player.