In the ever-evolving world of frontend development, simplicity contributes to building robust and maintainable applications. As new paradigms and patterns evolve to enhance our workflows it’s common to see capability and complexity increase at the same pace. In this article, we’ll discover a refreshing take on Angular development by combining the best parts of reactive state management and asynchronous development.
Promises vs Observables in Angular: Choosing the Right Tool for the Job
Observables have become a cornerstone in Angular development, providing a powerful mechanism for handling asynchronous operations. However, like any tool, they come with their complexities that developers must navigate. Let’s explore the dynamics between Promises and Observables in the Angular ecosystem.
Observables: Power and Complexity
Observables excel in scenarios involving multiple asynchronous events or continuous data streams. They provide a rich set of operators and features that enable developers to handle complex scenarios, such as debouncing, throttling, and handling multiple values over time.
While observables are incredibly powerful, they can sometimes introduce unnecessary complexity, especially for scenarios where a simpler solution might suffice. This complexity can be a double-edged sword, making code harder to read and understand, particularly for new developers entering into Angular.
Given the prominence of Observables in Angular, new developers often find themselves delving into the intricacies of RxJS, the library that powers Angular’s reactive programming paradigm. Learning RxJS becomes essential when dealing with Observables, but it also adds an additional layer of knowledge that developers need to acquire.
Promises: Simplicity in Single Asynchronous Calls
Promises, on the other hand, offer a more straightforward approach to handling single asynchronous operations. They are well-suited for scenarios where you expect a single value to be resolved or rejected. In Angular, both Promises and Observables can handle single asynchronous calls similarly.
There are more elegant means of demonstrating a single value Observable and RxJs but to compare the async/await approach we’ll use the following:
// Using a Promise async fetchDataWithPromise() { try { const data = await this.dataService.getDataAsPromise(); // Handle data as needed } catch (error) { // Handle errors gracefully } } // Using an Observable fetchDataWithObservable() { this.dataService.getDataAsObservable().subscribe( (data) => { // Handle data as needed }, (error) => { // Handle errors gracefully } ); }
Embracing Async/Await for Streamlined Data Fetching
Traditionally, Angular developers have heavily relied on observables for handling asynchronous operations with operators like switchMap, forkJoin, and others. While observables are a powerful tool, async/await provides an alternative means that in some scenarios can be more straightforward, providing synchronous-feeling logic to asynchronous code.
By integrating async/await into your Angular application, you can bring a new level of simplicity to your data fetching. This alternative approach allows developers to choose the retrieval pattern that best suits their application.
Let’s dive into an example to illustrate the power of async/await in action.
async loadProfile() { try { const user = await this.loadUser(); const billingProfile = await this.loadBillingProfile(user); this.patchState({ user, billingProfile }); } catch (e) { // handle error(s) } }
This simple yet powerful example demonstrates how async/await can streamline the asynchronous data fetching process, making your code more readable and maintainable. Now that we’ve talked about fetching data let’s discuss how we might manage application state.
Extending NgRx ComponentStore for Versatile State Management
NgRx is a widely adopted state management library for Angular applications, and the NgRx Component Store brings simplicity to the reactive state management landscape.
The NgRx Component Store allows you to encapsulate state management within your components, providing a clean and concise way to handle state change. At the same time, it allows developers to break away from the traditional observables-only mindset and adopt a more flexible approach to state management.
Don’t let the name fool you though, NgRx Component Store can be used in Services too! The NgRx docs describe it as:
a stand-alone library that helps to manage local/component state. It’s an alternative to reactive push-based “Service with a Subject” approach.
While capable, the Service with a Subject pattern on its own can quickly get complicated and mimic a mini-NgRx store depending on how involved and complicated the application gets. This is where the NgRx Component Store shines and lets you write powerful and simple Service with a Subject style services without adopting a full-blown NgRx pattern of actions, reducers, effects, etc. You might be asking yourself what’s the difference between the NgRx ComponentStore and Store. There’s an excellent write-up in the docs. For this example, we’re looking for a state management solution that’s simple, effective and doesn’t require recreating most of NgRx with actions, reducers, effects, and selectors. That’s what ComponentStore provides.
Creating a Store with NgRx ComponentStore
First, install the ComponentStore.
npm install @ngrx/component-store --save
Then create a user.store.ts file that extends the ComponentStore and provides the initial state in the constructor.
interface BillingProfile { id: string; } interface User { name: string; } interface UserProfileState { user: User; billingProfile: BillingProfile; } @Injectable({ providedIn: 'root' }) export class UserProfileStore extends ComponentStore<UserProfileState> { constructor() { // Set initial state super({ user: { name: '' }, billingProfile: { id: '' } }); }
Next, let’s add some observable properties to receive push-based updates to state changes.
export class UserProfileStore extends ComponentStore<UserProfileState> { readonly user$: Observable<User> = this.select((state) => state.user); readonly billingProfile$: Observable<BillingProfile> = this.select( (state) => state.billingProfile ); }
So far, we’ve defined our service, its state, and observables for other consumers to listen for updates with. Next, let’s introduce our state setting mechanism with async/await. First, we’ll define some contrived examples to simulate asynchronous network requests to fetch data in our store.
loadUser() { return this.fakePromise<User>({ name: 'Angular' }); } loadBillingProfile(user: User) { return this.fakePromise<BillingProfile>({ id: '1' }); } fakePromise<T>(data: T): Promise<T> { return new Promise((resolve) => setTimeout(() => { resolve(data); }, 300) ); }
Next, let’s assume one of our network calls relies on the other. We’ll add a method that coordinates how these calls are made and reuse our example form earlier.
async loadProfile() { try { const user = await this.loadUser(); const billingProfile = await this.loadBillingProfile(user); this.patchState({ user, billingProfile }); } catch (e) { // handle error(s) } }
Other Reactive Read Types
If you’re a seasoned NgRx developer, you’ve probably encountered a scenario where you want to read the state but don’t need or want an observable. It’s generally discouraged to rely on synchronously reading the state but a get method is available for doing that under certain circumstances.
get username() { return this.get(state => state.user.name); }
Lastly, we can also provide access to signals with the selectSignal method. This method is similar to select in that you can create a signal from a state projector function or by combining the provided signals.
readonly billingProfileId: Signal<string> = this.selectSignal( (state) => state.billingProfile.id );
The Total Package
Putting it all together here’s what our store looks like and what it achieves:
- It extends the NgRx ComponentStore which provides all the mechanisms to read and patch its local state on this in the class.
- Asynchronous data fetching is managed with async/await in an easy-to-read and maintainable format capable of error handling and interdependent data fetching.
- Access to state changes with push-based Observables and Signals as well as pull-based synchronous reads.
interface BillingProfile { id: string; } interface User { name: string; } interface UserProfileState { user: User; billingProfile: BillingProfile; } @Injectable({ providedIn: 'root' }) export class UserProfileStore extends ComponentStore<UserProfileState> { // Observables readonly user$: Observable<User> = this.select((state) => state.user); readonly billingProfile$: Observable<BillingProfile> = this.select( (state) => state.billingProfile ); // Signal readonly billingProfileId: Signal<string> = this.selectSignal( (state) => state.billingProfile.id ); constructor() { // Set initial state super({ user: { name: '' }, billingProfile: { id: '' } }); } // Pull-based read get username() { return this.get((state) => state.user.name); } // Orchestrate async methods async loadProfile() { try { const user = await this.loadUser(); const billingProfile = await this.loadBillingProfile(user); this.patchState({ user, billingProfile }); } catch (e) { // handle error(s) } } // Simulated data loading below this line loadUser() { return this.fakePromise<User>({ name: 'Angular' }); } loadBillingProfile(user: User) { return this.fakePromise<BillingProfile>({ id: '1' }); } clearState() { this.patchState({ user: { name: '' }, billingProfile: { id: '' } }); } fakePromise<T>(data: T): Promise<T> { return new Promise((resolve) => setTimeout(() => { resolve(data); }, 300) ); } }
Adding a small UI to interact with the store, this is how we might interact with our new store.
@Component({ selector: 'app-root', standalone: true, template: ` <h1>Hello from {{( userStore.user$ | async)?.name }}!</h1> <h2>Billing Profile: {{ userStore.billingProfileId()}}</h2> <button (click)="userStore.loadProfile()">Load</button> `, imports: [AsyncPipe] }) export class App { name = 'Angular'; userStore = inject(UserProfileStore) }
Conclusion
The combination of NgRx ComponentStore within a Service with a Subject pattern using async/await creates an interesting combination of simplicity and capability that’s able to support large enterprise applications yet be easy to reason about. RxJs, NgRx Store, and other patterns may be the right choice for your application but understanding these alternatives could help you achieve your goals faster and with fewer regressions when the situation allows.
A StackBlitz of this example can be found here. Happy Coding!