Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Twitter Inspired Follow System With Firebase and Angular

Episode 17 written by Jeff Delaney
full courses and content on fireship.io

The idea of user following and unfollowing has been around since the rise of Twitter, but add this feature into an app is not as easy as you might think. It is an inherently relational problem, so making it work with a NoSQL database requires some tradeoffs. In this lesson, we are going to build twitter-inspired follow and unfollow feature using on Firebase and Angular 4.

ng g service follow
ng g component user-profile

Prerequisites

You need to have the AngularFire2 package installed with user authentication configured to get started with followers. Our Firebase authentication system also save the user information to the database, so check the OAuth lesson if you get lost.

demo of firebase angular twitter follower feature

Modeling the User Followers in NoSQL

In a SQL database, you would model followers with a many-to-many relationship by saving the follower_id and followed_id into own table, then make a join operation to query the data.

Let’s first consider the two main questions we need to answer. (1) Is UserA following UserB? (2) How many followers does a user have?

In Firebase NoSQL, we can answer question 1 by saving by saving the userId of each followed user under their own ID. This will allow us to see who a user if following with a single query.

To answer the second question, we need to model the data in reverse. Each user’s followers will be saved under their Id.

following
followerUserId
followedUserId: true

followers
followedUserId
followerUserId: true

What about data duplication?

Wait, doesn’t this database structure just duplicate the followers? Yes, it does, but sometimes that is a tradeoff you need to make with NoSQL. Our goal is to make these queries as quickly as possible, and sometimes that can only be achieved with data duplication, which a small sacrifice to make for the overall agility of NoSQL. It is almost always better to improve the user experience at the expense of extra data storage.

Follow Service

The service needs to (1) get the followers of a user to build the follower count (2) get following of the currently logged in user, (3) update the relationship between two users, i.e follow and unfollow.

follow.service.ts

import { Injectable } from '@angular/core';
import { AngularFireDatabase } from 'angularfire2/database';

@Injectable()
export class FollowService {

constructor(private db: AngularFireDatabase) { }

getFollowers(userId: string) {
// Used to build the follower count
return this.db.object(`followers/${userId}`)
}

getFollowing(followerId:string, followedId:string) {
// Used to see if UserFoo if following UserBar
return this.db.object(`following/${followerId}/${followedId}`)
}

follow(followerId: string, followedId: string) {
this.db.object(`followers/${followedId}`).update({ [followerId]: true } )
this.db.object(`following/${followerId}`).update({ [followedId]: true } )
}

unfollow(followerId: string, followedId: string) {
this.db.object(`followers/${followedId}/${followerId}`).remove()
this.db.object(`following/${followerId}/${followedId}`).remove()
}


}

Component with Follow/Unfollow Button

We loop over a list list of users from the database, then the display the user UserProfileComponent. The parent component will pass the user and the currentUser to the child via the @Input decorator.

some-parent.component.html

You are logged in as <strong>{{currentUser?.displayName }}</strong><hr>
<div *ngFor="let user of users | async">
<user-profile [user]="user" [currentUser]="currentUser"></user-profile>
</div>

Each user profile has a follow/unfollow button and displays that user’s total follower count. We will generate two subscriptions that will define the isFollowing and followerCount variables with their emitted values.

The loadash function size will count the total number of keys in an collection. A caveat is that AngularFire2 will return { $value: null } when there is no data, which will make the follower count say 1 when it should actually be 0, which is why we have a countFollowers function defined separately.

The toggleFollow function is pretty self-explanatory. It triggers the service based on the current user’s relationship to this user profile.

It is also important to tear down these subscriptions with unsubscribe to when the component is destroyed to avoid memory leaks - especially if your app has a large number of users.

user-profile.component.ts

import { Component, OnInit, Input, OnDestroy } from '@angular/core';
import { FollowService } from "../../follow.service";
import { size } from "lodash";

@Component({
selector: 'user-profile',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.scss']
})
export class UserProfileComponent implements OnInit, OnDestroy {

@Input() user; // a user who can be followed
@Input() currentUser; // currently logged in user

followerCount: number;
isFollowing: boolean;

followers;
following;



constructor(private followSvc: FollowService) { }

ngOnInit() {
const userId = this.user.$key
const currentUserId = this.currentUser.uid


// checks if the currently logged in user is following this.user
this.following = this.followSvc.getFollowing(currentUserId, userId)
.subscribe(following => {

this.isFollowing = following.$value

})

// retrieves the follower count for a user's profile
this.followers = this.followSvc.getFollowers(userId)
.subscribe(followers => {

this.followerCount = this.countFollowers(followers)

})
}


private countFollowers(followers) {
if (followers.$value===null) return 0
else return size(followers)
}


toggleFollow() {
const userId = this.user.$key
const currentUserId = this.currentUser.uid

if (this.isFollowing) this.followSvc.unfollow(currentUserId, userId)
else this.followSvc.follow(currentUserId, userId)
}


ngOnDestroy() {
this.followers.unsubscribe()
this.following.unsubscribe()
}


}

In the template, we can use the Angular 4 if-then-else syntax to display the corresponding button for the current user.

user-profile.component.html

<div class="profile">
<img [src]="user.img" width="50px">
@{{user.username}} has <strong>{{followerCount}}</strong> followers
<div *ngIf="isFollowing; then unfollowButton else followButton">
button renders here
</div>
</div>

<ng-template #followButton>
<button class="button is-info" (click)="toggleFollow()">Follow</button>
</ng-template>

<ng-template #unfollowButton>
<button class="button is-warning" (click)="toggleFollow()">Unfollow</button>
</ng-template>

Extra Credit - Scaling up

What happens when your app gets huge and Justin Beiber has 500 million followers? The database queries will start to drag down performance. A better way to handle the the user count is with a background task that updates the count whenever there is new activity. Firebase cloud functions allows you to run tasks wherever their is a new write operation. This would allow you to save a user count for a user in the background, then you would only need a single key-value pair to to get the follower count.

userId
followerCount: number

Check out my cloud functions onWrite lesson. For details on implementing this type of cloud function.

That’s it for NoSQL follow unfollow.