Using Firebase for user presence tracking
Introduction #
Firebase handles so many core infrastructure needs that modern apps have, ranging from persistence to authentication to offline mode. This tutorial is all about how to leverage Firebase to build a presence system for signed in users. The idea is for your app to sense when a signed in user is online, idle, away, and offline. So not only do you see which users are currently online, but also what their detailed state is. Both idle and away are forms of being online. Offline might mean that the user has closed the app or they don’t have network connectivity.
To see this app in action, click here and then sign in to the app.
How this is built #
For details on how this is done, check out presence.ts
on
GitHub.
This is part of a todo list app that is built using
React, Redux, Firebase, and Typescript.
The main thing that drives presence track is Firebase’s ability to sense if the client app (on any platform) is currently online or offline. So it can track network state on the client app or device that the Firebase client is running on. It can also respond on the server side to network connectivity changes (eg: when the client device loses network, or the app is closed).
Sensing network connectivity #
There’s a special path that you can create a Firebase reference to (in your client app) that listens
to changes in network connectivity on the device that this client is running on (web browser on a
laptop, or native app on a mobile device). Here’s the location: .info/connected
. This is what it
looks like to attach a value change listener to this path.
const userListRef = firebase.database().ref("USERS_ONLINE")
const myUserRef = userListRef.push()
// Monitor connection state on browser tab
firebase
.database()
.ref(".info/connected")
.on("value", function (snap) {
if (snap.val()) {
// if we lose network then remove this user from the list
myUserRef.onDisconnect().remove()
// set user's online status
setUserStatus("online")
} else {
// client has lost network
setUserStatus("offline")
}
})
This is what is happening in the code above:
-
At the start of code block, a
userListRef
object is created that will be used to store an object for each user that is active in the app.myUserRef
is a pointer to this user object that is returned bypush()
. The idea here is that all the users that are online in the system have an entry in thisuserListRef
node in Firebase. And as they go offline, this entry is removed from Firebase. This is what allows your app to sense which users are online and which ones have gone offline (more on this below). -
When Firebase client senses that network connectivity state changes the callback is invoked. The
snap
object containstrue
orfalse
. This happens in your client app and this callback runs in your client app. -
When you have network connectivity (
snap.val()
istrue
) and your callback is run, it registers aonDisconnect()
handler on themyUserRef
object. What this does is that it tells Firebase toremove()
themyUserRef
on the SERVER side when Firebase senses that the client is no longer connected!-
When
snap.val()
becomes false and your callback is run, it will actually only run this callback in your client app (in your browser tab). Since network is lost, Firebase server will not be notified (obviously). -
However, Firebase server will sense (after a while) that the Firebase client isn’t connected to it anymore. And it will run the
remove()
function on the SERVER side!- When the client comes back online, this object will be removed from the client side (since
they will sync). However, when
setUserStatus()
runs it will actually re-create this object on the client with the same key that it had before. So when you go fromonline
->offline
->online
again, it all works out! However, they work out differently on the client side than the server side. Whew! This is pretty intricate and Firebase essentially makes it as simple as it can for you to deal with this.
- When the client comes back online, this object will be removed from the client side (since
they will sync). However, when
-
What does setUserStatus() do? It just takes the currently signed in user and the status (online, away, idle, and offline) and writes it to myUserRef.
let presenceObject = { user: myUserObject, status: myStatus }
myUserRef.set(presenceObject)
Responding to user state changes #
The code above simply allows you to sense network connectivity changes in your own client app, but
what of the other users who are connected at the same time? There is another half to this
presence.ts
file, which is how to you detect when other users have come online or when their
status is changing or when they go offline entirely.
// update the UI to show that a new user is now online
userListRef.on("child_added", function (snap) {
const presence: PresenceIF = snap.val()
ctx.emit(GLOBAL_CONSTANTS.LE_PRESENCE_USER_ADDED, presence)
})
// update the UI to show that a user has left (gone offline)
userListRef.on("child_removed", function (snap) {
const presence: PresenceIF = snap.val()
ctx.emit(GLOBAL_CONSTANTS.LE_PRESENCE_USER_REMOVED, presence)
})
// update the UI to show that a user's status has changed
userListRef.on("child_changed", function (snap) {
const presence: PresenceIF = snap.val()
ctx.emit(GLOBAL_CONSTANTS.LE_PRESENCE_USER_CHANGED, presence)
})
-
This block of code just adds 3 callbacks to listen to various child event changes to the userListRef reference to Firebase (remember that this holds entries for every user that is online and when they go offline this entry is removed).
-
In each of the callbacks a different event is emitted that notifies the rest of the system that these changes have occurred. And in this example, the
groupchat.tsx
file is what responds to these events and paints the UI with these changes. -
To learn more about the event emitter (observer / observable) pattern, please read this tutorial.
UI updates #
The groupchat.tsx
file
(GitHub)
simply attaches listeners to the PRESENCE_USER_ADDED
, PRESENCE_USER_REMOVED
, and
PRESENCE_USER_CHANGED
events. This is what that code looks like:
// respond to changes in presence
applicationContext.addListener(GLOBAL_CONSTANTS.LE_PRESENCE_USER_ADDED, (presence: PresenceIF) => {
const msg: ChatMessageIF = {
message: `${presence.user.displayName} joined`,
displayName: "The App",
photoURL: "https://url/image.png",
timestamp: new Date().getTime(),
}
this.rcvMsgFromServer(msg)
})
applicationContext.addListener(
GLOBAL_CONSTANTS.LE_PRESENCE_USER_REMOVED,
(presence: PresenceIF) => {
const msg: ChatMessageIF = {
message: `${presence.user.displayName} left`,
displayName: "The App",
photoURL: "https://url/image.png",
timestamp: new Date().getTime(),
}
this.rcvMsgFromServer(msg)
}
)
applicationContext.addListener(
GLOBAL_CONSTANTS.LE_PRESENCE_USER_CHANGED,
(presence: PresenceIF) => {
const msg: ChatMessageIF = {
message: `${presence.user.displayName} is ${presence.status}`,
displayName: "The App",
photoURL: "https://url/image.png",
timestamp: new Date().getTime(),
}
this.rcvMsgFromServer(msg)
}
)
There’s an important thing that happens in rcvMsgFromServer()
that is worth noting, and this has
to do with using setState()
in React. Make sure NOT to re-use partial state to create new state.
Just make a new state object and setState()
with it, and let React figure out what needs to be
rendered / re-rendered, otherwise, you will confuse React with what it should mark as dirty and
needing re-rendering.
rcvMsgFromServer(data: ChatMessageIF) {
const {chatMessageList} = this.state;
let copy = lodash.clone(chatMessageList);
copy.push(data);
this.setState({chatMessageList: copy});
}
Getting the code #
You can get the code on GitHub.
👀 Watch Rust 🦀 live coding videos on our YouTube Channel.
📦 Install our useful Rust command line apps usingcargo install r3bl-cmdr
(they are from the r3bl-open-core project):
- 🐱
giti
: run interactive git commands with confidence in your terminal- 🦜
edi
: edit Markdown with style in your terminalgiti in action
edi in action