I refactored my Angular and Firebase Sign up App from RxJS to Promises!

6 min read


Refactoring Angular Code: From Observables to Promises

In a previous blog, we started refactoring our signup application, incorporating a better understanding of the new Signals API. In this latest segment, we take a step further and expand our refactoring spree to data modification services. This isn't related to Signals, but will be helpful particularly if you are coming from a JavaScript background.

Video Tutorial

The app we want to refactor

The primary task here is converting our Observables into Promises. For newcomers to Angular, Observables can be a confusing concept. Although they are incredibly powerful, Promises can often be more intuitive, especially for anyone familiar with JavaScript.

Let's dive further into the refactoring process to demystify some of these concepts.

This is the app that we're working on, in case you need the code to walkthrough :)

Removing and Replacing Observables

At the start of our refactoring adventure, we find ourselves staring at three services: the Auth service, User service, and Image Upload service. The Auth service is where we begin. In the auth service, we find the from utility function from RxJS, which allows conversion of the Firebase promise-based API into an Observable.

signUp(email: string, password: string): Observable<UserCredential> {
  return from(createUserWithEmailAndPassword(this.auth, email, password));
}

login(email: string, password: string): Observable<any> {
  return from(signInWithEmailAndPassword(this.auth, email, password));
}

Our first action is to remove that. On its removal, the resulting function will then need to return a Promise instead of an Observable.

signUp(email: string, password: string): Promise<UserCredential> {
  return createUserWithEmailAndPassword(this.auth, email, password);
}

login(email: string, password: string): Promise<any> {
  return signInWithEmailAndPassword(this.auth, email, password);
}

Recall again, we were converting the promises to observables here for consistency. Now that we're using signals, we don't actually need to use observables anywhere, except when strictly needed for async reactivity.

Simplifying the User Service

In the User Service, we come across a few functions. The task is simple: convert the Observable into a Promise. The refactoring pattern is almost similar to the Auth service.

addUser(user: ProfileUser): Promise<void> {
  const ref = doc(this.firestore, 'users', user.uid);
  return setDoc(ref, user);
}

updateUser(user: ProfileUser): Promise<void> {
  const ref = doc(this.firestore, 'users', user.uid);
  return updateDoc(ref, { ...user });
}

Once done with the User service, let's now navigate to the Image Upload service.

Enriching the Image Upload Service

Refactoring the Image Upload service presents us with a slightly more complex task. However, breaking it down, it becomes straightforward. Here, the function needs to return a Promise as before. But due to multiple operations that need to be performed, we'll need to convert the function to async and use the await syntax inside to wait for the operations to complete.

async uploadImage(image: File, path: string): Promise<string> {
  const storageRef = ref(this.storage, path);
  const result = await uploadBytes(storageRef, image);
  return await getDownloadURL(result.ref);
}

Note how the function is simpler to understand now with a clear code path and what is being returned and used in the next step.

Refactoring the components

Finally, after having refactored the services from Observables to Promises, we need to update the components to use these promises with the async-await syntax.

Every Observable should switch to a Promise, which requires adding a try-catch block to handle potential errors. Let's look at the login component. I've added some comments from where the toaster will be needed.

async submit() {
  ...
  // Show loading
  try {
    await this.authService.login(email, password);
    // Show success
    this.router.navigate(['/home']);
  } catch (error: any) {
    // Show error
  } finally {
    // Hide loading
  }
}

So again, you can see we can clearly see the code path going from one operation to the other and there are no RxJS streams to get our heads around.

But what do we do for the toaster messages? Because the original method observe we were using is meant to be used inside an observable stream.

this.authService
  .login(email, password)
  .pipe(
    this.toast.observe({
      success: 'Logged in successfully',
      loading: 'Logging in...',
      error: ({ message }) => `There was an error: ${message} `,
    })
  )
  .subscribe(() => {
    this.router.navigate(['/home']);
  });
}

So let's create a new service called toast service that encapsulates this functionality and provides easy to use functions to show and hide the toasters we need.

Creating the Toast Service

Our toaster service will contain separate functions to show and hide the loader and the success and failure messages.

@Injectable({
  providedIn: "root",
})
export class ToastService {
  toast = inject(HotToastService);

  showLoader(message: string) {
    this.toast.loading(message, { id: "loader" });
  }

  hideLoader() {
    this.toast.close("loader");
  }

  showSuccess(message: string) {
    this.toast.success(message);
  }

  showError(message: string) {
    this.toast.error(message);
  }
}

Pretty simple and easy to understand!

Let's plug it in our login component and complete the functionality.

this.toastService.showLoader("Logging in...");
try {
  await this.authService.login(email, password);
  this.toastService.showSuccess("Logged in successfully");
  this.router.navigate(["/home"]);
} catch (error: any) {
  this.toastService.showError(`There was an error: ${error?.message} `);
} finally {
  this.toastService.hideLoader();
}

Great, so we've completely refactored the login component to using promises!

Completing the other components

For the Sign Up component, this is what we get in the end.

async submit() {
  ...

  this.toastService.showLoader('Signing in...');
  try {
    const {
      user: { uid },
    } = await this.authService.signUp(email, password);
    await this.usersService.addUser({ uid, email, displayName: name });
    this.toastService.showSuccess('Congrats! You are all signed up');
    this.router.navigate(['/home']);
  } catch (error: any) {
    this.toastService.showError(error?.message);
  } finally {
    this.toastService.hideLoader();
  }
}

Again, notice how it is simpler for beginners who're not aware of RxJS observables to see where we're getting and sending data to using the Promise based syntax!

The last one is the profile component and I'll leave you to do this yourself, because it'll be a good learning experience. You can check out the final code on this link.

Testing the app after refactor

After systematically working through the entire refactoring process, test everything to ensure that the program runs smoothly.

  • Test simple login
  • Test sign up of a new user
  • Test editing a profile information
  • Test uploading of a new profile picture

Profile screen during testing

Summarizing Our Code Refactoring

This two-part series has taken us on a journey from Observable-based services in Angular through to Services using Promises instead. You can find the complete application and the respective code via this link.

For me personally, I believe the refactoring was worth it. It makes for cleaner, more readable code, especially for beginners in Angular.

However, the jury is still out: whether the refactoring effort makes the code better depends on your familiarity with RxJS and Observables. Regardless, understanding both Observables and Promises will serve you well in long run!

Comment below if you have any further thoughts or suggest any other topics you'd like to discuss.

Happy coding!

Support

Liked this post? You can show your love by buying me a coffee - so I can keep bringing you more posts like this. Thanks in advance! 😊

You may also like...