Handling realtime data storage in Angular using Firebase Cloud Firestore

Post Editor

This article will give you a step by step walkthrough on implementing a realtime database in your Angular Apps using Firebase Cloud Firestore.

9 min read
0 comments
post

Handling realtime data storage in Angular using Firebase Cloud Firestore

This article will give you a step by step walkthrough on implementing a realtime database in your Angular Apps using Firebase Cloud Firestore.

post
post
9 min read
0 comments
0 comments

This is the third article in a series of articles where Martina(who's a fellow GDE in Angular & Web Tech) and I create KittyGram: A super-minimal Instagram Clone that allows uploading only Cat Photos. Please find more information regarding the project overview in my previous article. And you can find more information about what we've implemented so far in this article by Martina.

For the scope of this article, we'll mainly implement features to:

  • Store the image URL and metadata related to it in Firebase Cloud Firestore.
  • Read this data as a list to populate our feed.

Wanna save time and jump directly to a specific section of this article? I've got you covered:

Setting up Firebase Cloud Firestore
Link to this section

In the Firebase Console
Link to this section

So first up, we need to set up Cloud Firestore on the Firebase Console. Doing that is pretty straightforward. Just follow the steps below:

Develop > Database > Create Database(in the Header) > Start in Production Mode > Select a Cloud Firestore Location > Done

Content imageContent image
Setting up Firebase Cloud Firestore on the Firebase Console

Also, our Cloud Firestore Database doesn't really allow any writes at the moment. If you navigate to the Rules tab, the current rules are set like:

Content imageContent image
Initial Cloud Firestore Security Rules
<>Copy
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if false; } } }
Initial Cloud Firestore Security Rules

As you can see in the rule, allow read, write: if false; would mean:

  • allow read as in, reads are allowed.
  • write: if false; i.e. writes are not allowed.

Now, we do need to allow reads to the data even by unauthenticated users. But we only intend to allow writes by authenticated users. So we can update the rules to this:

<>Copy
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read: if true; allow write: if request.auth.uid != null; } } }
Final Cloud Firestore Security Rules

Once you update the rules, it would look something like this:

Content imageContent image
Final Cloud Firestore Security Rules

In our Angular App
Link to this section

In order to interact with the Cloud Firestore, we also need to have access to the relevant APIs from the @angular/fire package. All these APIs are exposed as a part of the AngularFirestoreModule. So we'll have to add that as well, to the exports array of our AppFirebaseModule.

<>Copy
... import { AngularFirestoreModule } from '@angular/fire/firestore'; ... @NgModule({ ... exports: [ ... AngularFirestoreModule, ... ], }) export class AppFirebaseModule {}
app-firebase.module.ts

Now that we've added that to the exports array, we'll be able to use these exposed APIs in the declarables(Components, Pipes, and Directives) and services registered on our AppModule.

Storing post data in the Firestore
Link to this section

Perfect! Now that we have the Firestore base set-up, let's now work on leveraging it to store data in our Firebase Cloud Firestore.

The things that we might want to store for a particular post are:

  • Name and Avatar URL of the User posting the Cat photo.
  • Photo URL and Description from the Post.
  • Last Updated timestamp.
  • Number of likes(that we're calling purrs).
  • An Id(that would be generated by Cloud Firestore) that we can refer to this post by.

We'd generally create a model interface for this for type-safety reasons. So let's create it in the app/models folder:

<>Copy
export interface UserPost { description: string; id?: string; lastUpdated: number; photoUrl: string; purrs: number; userAvatar: string; userName: string; doc?: any; }
user-post.model.ts

I'm sure you've noticed that the id field is optional. That's because we won't have the id while creating a post. But we will have it while reading it.

You might have also noticed the doc optional field which is of type any. We'll get back to it when we're implementing lazy loading in the next article.

Alright, next up we need a way to connect to the Firebase Cloud Firestore and then store the data in there. To do that, we can use the AngularFirestore service exposed by the AngularFirestoreModule. We can create a service to encapsulate this business logic. Let's call it DatabaseService.

In our DatabaseService once we inject the AngularFirestore service as a dependency, we can then:

  • Create a new collection by calling the collection method on it. It returns an instance of type AngularFirestoreCollection.
  • Retrieve items in the collection. This collection also has a valueChanges method on. Calling it would return an Observable of the whole collection.
<>Copy
import { AngularFirestore, AngularFirestoreCollection, DocumentReference, } from '@angular/fire/firestore'; import { Injectable } from '@angular/core'; import { Observable, from } from 'rxjs'; import { UserPost } from './../../models/user-post.model'; @Injectable({ providedIn: 'root', }) export class DatabaseService { private userPostsCollection: AngularFirestoreCollection<UserPost>; userPosts$: Observable<UserPost[]>; constructor(private afs: AngularFirestore) { this.userPostsCollection = afs.collection<UserPost>('user-posts'); this.userPosts$ = this.userPostsCollection.valueChanges({ idField: 'id' }); } ... }
database.service.ts

As you can also see here, we have a userPosts$ Observable that we're exposing for the FeedComponent to show us the feed.

This service also needs:

  • A method to add a post to the collection.
  • A method to update an existing post.

The userPostsCollection has an add method that we can call to add a new UserPost to the collection. Something like this:

<>Copy
addUserPost(userPost: UserPost): Observable<DocumentReference> { return from(this.userPostsCollection.add(userPost)); }
addUserPost Method

Regarding the update, we need it coz we want to update the purrs(likes) on a post once a user clicks on the purr button.

We can call the doc method on the AngularFirestore with user-posts/postId to create a reference to that document in the collection. Doing that will return a reference to that document on which we can call the update method passing the partial update user post as an argument. Something like this:

<>Copy
updatePost(userPost: UserPost): Observable<void> { return from( this.afs.doc<UserPost>(`user-posts/${userPost.id}`).update({ purrs: ++userPost.purrs, }), ); }
updatePost Method

Awesome! So we now have the DatabaseService in place that we can inject as a dependency in the CreateComponent to store the created post. We can simply call the addUserPost after creating a UserPost Object.

Since the addUserPost method returns Observable<DocumentReference>, we'll just pipe through the downloadUrl$ and switch the context using the switchMap operator.

Also, as we don't want the Observable stream to die and we're handling the error using the catchError operator already, we'll now return of(null) from there instead of EMPTY.

That way, we can filter the Observable stream to just pass through if the value is not null. That's what the usage of filter operator in there does. The rest is pretty much the same. After making all these changes, our CreateComponent class would look something like this:

<>Copy
... import { Observable, of, Subject } from 'rxjs'; import { catchError, filter, switchMap, takeUntil } from 'rxjs/operators'; ... import { AuthService } from '../../services/auth/auth.service'; import { DatabaseService } from './../../services/database/database.service'; ... import { UserPost } from './../../models/user-post.model'; ... @Component({ ... }) export class CreateComponent implements OnInit, OnDestroy { ... constructor( ... private readonly databaseService: DatabaseService, ... ) {} ... postKitty() { ... downloadUrl$ .pipe( switchMap((photoUrl: string) => { const userPost: UserPost = { userAvatar: this.user.photoURL, userName: this.user.displayName, lastUpdated: new Date().getTime(), photoUrl, description: this.pictureForm.value.description, purrs: 0, }; return this.databaseService.addUserPost(userPost); }), catchError((error) => { this.snackBar.open(`${error.message} 😢`, 'Close', { duration: 4000, }); return of(null); }), filter((res) => res), takeUntil(this.destroy$), ) .subscribe((downloadUrl) => { this.submitted = false; this.router.navigate([`/${FEED}`]); }); } ... }
create.component.ts

Regarding the UserPost the purrs(likes) are set to 0. Rest all the fields are pretty straightforward. Please comment down below if you have any doubts/confusion regarding it.

Let's quickly test if our application in the current state works as expected or not.

Content imageContent image
Testing Storage and Database Connection

And, it does. As you can see, initially on the console for both the Storage and Database section, there isn't anything. But as soon as you upload a post from our Angular App, the image gets stored in the storage bucket and the data is stored in the Cloud Firestore.

Purrfect! Now that we have a way to store a UserPost. Let's now figure out a way to read this stored data.

Reading data for the feed from Firestore
Link to this section

Reading data is actually pretty straightforward. We already have the userPosts$ property(which is a collection of user posts), exposed as an Observable from the DatabaseService. We can inject the DatabaseService as a dependency in our FeedComponent and leverage this userPosts$ to populate our feed. Something like this:

<>Copy
import { Component } from '@angular/core'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { DatabaseService } from './../../services/database/database.service'; import { UserPost } from './../../models/user-post.model'; @Component({ ... }) export class FeedComponent { userPosts$: Observable<Array<UserPost>> = this.databaseService.userPosts$; constructor(private readonly databaseService: DatabaseService) {} handlePurrClick(userPost: UserPost) { this.databaseService.updatePost(userPost).pipe(take(1)).subscribe(); } }
feed.component.ts

For the feed, we also need to have a child presentational/dumb Component that could take the UserPost as a @Input and emit the click on the purr(like) button as an @Output event. We'll just create one and call it FeedItemComponent.

Also, since this is a presentational/dumb Component let's use the ChangeDetectionStrategy of OnPush in here.

It would look something like this:

<>Copy
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter } from '@angular/core'; import { UserPost } from './../../models/user-post.model'; @Component({ selector: 'app-feed-item', templateUrl: './feed-item.component.html', styleUrls: ['./feed-item.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class FeedItemComponent { @Input() userPost: UserPost; @Output() purrClick: EventEmitter<UserPost> = new EventEmitter<UserPost>(); handlePurr() { this.purrClick.emit(this.userPost); } }
feed-item.component.ts

The template for this Component would just be a regular Card, something like this:

<>Copy
<mat-card class="feed-item-card"> <mat-card-header> <div mat-card-avatar> <img class="avatar" [src]="userPost.userAvatar" /> </div> <mat-card-title>{{ userPost.userName }}</mat-card-title> <mat-card-subtitle>{{ userPost.lastUpdated | date }}</mat-card-subtitle> </mat-card-header> <img mat-card-image class="preview-image" [src]="userPost.photoUrl" alt="Photo of a cute Kitty 😻" /> <mat-card-content> <p> {{ userPost.description }} </p> </mat-card-content> <mat-card-actions> <button mat-button (click)="handlePurr()">{{ userPost.purrs }} 😻</button> </mat-card-actions> </mat-card>
feed-item.component.html

With that in place, we can now, in the feed.component.html, just unwrap the userPosts$ Observable using an async pipe, iterate through the unwrapped array using the *ngFor directive, and render the app-feed-item component passing it one userPost at a time.

<>Copy
<div class="container"> <ng-container *ngIf="userPosts$ | async as userPosts"> <app-feed-item *ngFor="let userPost of userPosts" [userPost]="userPost" (purrClick)="handlePurrClick($event)" > </app-feed-item> </ng-container> </div>
feed.component.html

We will also listen to the purrClick @Output event from the FeedItemComponent by calling the handlePurrClick method on it.

Awesome, we now have everything in place. We can add items to our database and we can also list them out in the feed and see if a logged-in user could update purrs on a cat post by liking it.

Content imageContent image
Edit Post works too!

Awesome! Our App looks and behaves as expected. BUT, there's an issue with it at the moment.

Since our userPosts$ is a valueChanges call on our AngularFirestoreCollection we'll basically get everything that we have in our Cloud Firestore every time there's a change.

This isn't such a big deal for just one or two or maybe even 10 posts. But what if our Cloud Firestore has 1M records(Who knows? It might). At the moment, these 1M records would be downloaded on the initial load of the App. And that's crazy, right? It would just kill our App's load time. So, what do we do?

Next Steps
Link to this section

Well, remember this optional doc property of type any that we had in our UserPost model. That's something that could be used to potentially paginate our feed and request for new data as the user scrolls through and reached the end of the current batch of data.

That's exactly what we'll be implementing as a part of the next article. So stay tuned!

Closing Notes
Link to this section

And that brings us to the end of this article. Thanks for sticking around. I hope you liked it.

A big shout-out to Martina Kraus for proofreading this and collaborating with me on this project.

I hope this article taught you something new related to Angular and Firebase. If it did, share this article with your friends and peers who are new to Angular and want to achieve something similar.

Please find the GitHub repo here.

As always, if you had any doubts, questions, suggestions or constructive criticism, please comment them down below. Until next time then.

Comments (0)

Be the first to leave a comment

Share

About the author

author_image

Siddharth is a FullStack JavaScript Developer currently based out in Sydney, Australia. He's a Google Developer Expert in Angular and Web Technologies. He speaks about and teaches Angular on Udemy.

author_image

About the author

Siddharth Ajmera

Siddharth is a FullStack JavaScript Developer currently based out in Sydney, Australia. He's a Google Developer Expert in Angular and Web Technologies. He speaks about and teaches Angular on Udemy.

About the author

author_image

Siddharth is a FullStack JavaScript Developer currently based out in Sydney, Australia. He's a Google Developer Expert in Angular and Web Technologies. He speaks about and teaches Angular on Udemy.

Featured articles