How to Rewrite Angular Apps to be Nearly Observable and Subscribe-Free
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.
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.
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.