Firebase provides realtime streams and async callbacks to do all sorts of awesome things in your app. Getting data from one realtime stream is easy enough, but what if you want to join it from another? Combining multiple async streams of data can get complex. At Firebase, we wanted to help simplify things, so we created a new JavaScript library: RxFire.
RxJS is a popular JavaScript library that simplifies async streams of data. The core component of RxJS is the observable. The observable is a mechanism for observing data as it changes over time. RxJS allows you to transform the changes as they occur with array like operators such as map and filter. This works magically with Firebase.
RxJS is a powerful toolkit for observing and transforming data streams. Firebase provides multiple types of data streams. Combine the two and you have a simple way of managing async data across multiple Firebase streams. Need to get data from an authenticated user? Want to join two records in Firestore? Want to lazy load the Firebase SDK with Webpack? This is the magic we're talking about. We wanted to capture this magic in a library: RxFire.
RxFire provides a set of observable creation methods. You simply call a function with some parameters to receive an RxJS observable. With that observable in hand you can use any operators provided by RxJS to transform the stream as you like.
The observable has a .pipe() method that is used to attach operators. This is really similar to an array. You can .map() and .filter(), but the magic is that as data comes in over time it is ran through these operators. This is great for situations where you want to listen for when a user logs in.
.pipe()
.map()
.filter()
import firebase from 'firebase/app'; import 'firebase/auth'; import { authState } from 'rxfire/auth'; import { filter } from 'rxjs/operators'; const app = firebase.initializeApp({ /* config */ }); authState(app.auth()) .pipe( filter(u => u !== null) ).subscribe(u => { console.log('the logged in user', u); });
The code above uses RxFire to listen to authentication state. The .filter() operator checks for an authenticated user. The .subscribe() callback is triggered only if a user is present.
.subscribe()
RxFire also includes helpful functions for common Firebase tasks. Do you need a synchronized array of child events in the Realtime Database? You're just one function call away!
import firebase from 'firebase/app'; import 'firebase/database'; import { list } from 'rxfire/database'; const app = firebase.initializeApp({ /* config */ }); const todosRef = app.database().ref('todos'); list(todosRef).subscribe(list => { console.log('a synchronized array!', list); });
Events occurring on the todoRef trigger the subscribe callback with a new array.
todoRef
The idea of mixing RxJS and Firebase together is not revolutionary or unique. The officially supported AngularFire library heavily uses RxJS with Firebase. We took the lessons we learned from over two years of development and ported it out so all frameworks could benefit. It doesn't matter if you're using React, Preact, Vue, Stencil, Polymer, or just plain old JavaScript. RxFire is setup to be a simple library to simplify the Firebase streams. Just look at this simple Preact component.
import { Component, h } from 'preact'; import firebase from 'firebase/app'; import 'firebase/firestore'; import { collectionData } from 'rxfire/firestore'; export class AppComponent extends Component { componentWillMount() { this.app = firebase.initializeApp({ /* config */ }); this.todosRef = app.firestore().collection('todos'); collectionData(todosRef, 'id').subscribe(todos => { // re-render on each change this.setState({ todos }); }); } render() { const lis = this.state.todos.map(t => <li key={t.id}>{t.title}</li>); return ( <div> <ul> {lis} </ul> </div> ); } }
The component renders new list items each time the collection data changes.
Notice that RxFire isn't a complete wrapper for Firebase and RxJS, rather it works in a complementary fashion. This makes is easy to sprinkle in to existing applications or even build new integrations.
Let's say you want to create an idiomatic library for your app or framework. You can use RxFire as a base layer and let it do the heavy lifting for you. All you have to do is place your own API on top of it instead of worrying about complex tasks like synchronizing multiple events into a single array.
A common task is joining data in either the Realtime Database or Firestore. This is difficult because you're trying to wrangle data from multiple callbacks. Each callback can trigger at different times and it becomes a strain to figure out what's going on.
RxFire simplifies combining multiple data sources. You don't have to worry about nested callbacks or wrangling variables that are out-of-scope. All you need to do is use RxJS's observable combination methods like merge or combineLatest. In fact, combineLatest works just like a join.
merge
combineLatest
import firebase from 'firebase/app'; import 'firebase/firestore'; import { collectionData, docData } from 'rxfire/firestore'; import { combineLatest } from 'rxjs'; const app = firebase.initializeApp({ /* config */ }); // Create observables of document and collection data const profile$ = docData(app.firestore().doc('users/david')); const cart$ = collectionData(app.firestore().collection('carts/david')); const subscription = combineLatest(profile$, cart$, (profile, cart) => { // transform the profile to add the cart as a child property profile.cart = cart; return profile; }) .subscribe(profile => { console.log('joined data', profile); }); // Unsubscribe to both collections in one call! subscription.unsubscribe();
The combineLatest method waits for each observable to fire once. It then triggers the subscription callback each time a change occurs to either the profileRef or cartRef. The last argument allows you to transform the data into a single object, making joins much easier.
profileRef
cartRef
Retrieving data from an authenticated user is another common task that traditionally required nested callbacks. RxJS allows you to get one piece of data asynchronously and then use that to conditionally get another. In this case you can check for an authenticated user and then retrieve a photo from Cloud Storage based on the user's id.
import firebase from 'firebase/app'; import 'firebase/auth'; import 'firebase/storage'; import { authState } from 'rxfire/auth'; import { getDownloadURL } from 'rxfire/storage'; import { switchMap, filter } from 'rxjs/operators'; const app = firebase.initializeApp({ /* config */ }); authState(app.auth()) .pipe( filter(u => u !== null), switchMap(u => { const ref = app.storage().ref(`profile/${u.uid}`); return getDownloadURL(ref): }); ).subscribe(photoURL => { console.log('the logged in user's photo', photoURL); });
The code above listens for the authenticated users and filters out any logged out or null events. When there is an authenticated user, we get their photo URL based on the logged in user's id.
Modern web tools like Webpack provide advanced tooling like code-splitting. Code-splitting is an easy way to lighten the load of your application's core code. The idea is to keep only what's needed in the core of the application and load the rest only when needed. This is achieved by using the dynamic import() statement.
document.querySelector('#btnLoadFirebase') .addEventListener('click', event => { // load Firebase only when this button is clicked! import('firebase/app') .then(firebase => { console.log('lazy loaded!' }); });
Webpack is super smart and takes the lazily loaded module ('firebase/app' in this case) and splits it out into a separate file. This is a huge benefit for performance because it lightens to amount of JavaScript the browser has to load upfront.
'firebase/app'
While this technique is great for performance, it's really tricky to get right. You may have to load multiple libraries and deal with multiple async callbacks. RxFire makes this much easier. Using RxFire you can simplify the loading The dynamic import() uses promises, but RxJS makes it easy to convert promises to observables.
import()
import { from, combineLatest } from 'rxjs'; import { map, mergeMap } from 'rxjs/operators'; function lazyLoadCollection(config, collectionName) { const app$ = from(import('firebase/app')); const firestore$ = from(import('firebase/firestore')); const rxfire$ = from(import('rxfire/firestore')); return combineLatest(app$, firestore$, rxfire$) .pipe( map(([firebase, firestore, rxfire]) => { const app = firebase.apps[0] || firebase.initializeApp(config); return { app, rxfire }; }), mergeMap(([app, rxfire]) => { const ref = app.firestore().collection(collectionName); return rxfire.collectionData(ref, 'id'); }) ); } document.querySelector('#btnLoadTodos') .addEventListener('click', event => { lazyLoadCollection({ /* config */ }, 'todos').subscribe(todos => { console.log('lazy todos!', todos); }); });
The sample above creates a stream of todos from Firestore while lazily loading Firebase, RxFire, and RxFire on a button click. This type of code is still more complex that we'd like, but RxFire is in beta and we're working on making this much easier.
We'd love for you to kick the tires on RxFire. Give it an install on npm or yarn. Make sure to include Firebase and RxJS as they are peer dependencies.
npm i rxfire firebase rxjs # or yarn add rxfire firebase rxjs
RxFire is in beta. We really value your feedback in any form. File issues or feature requests on Github and even contribute your own ideas and code! We're really excited to see what you'll do with RxFire.