Live data with Firebase and redux-saga
Firebase is a backend-as-a-service offering from Google that aims to provide some of the common server-side functionality usually created for apps. Firebase provides a JSON-based key:value database which has a pretty nifty feature — live updates pushed to your front-end whenever the database is updated.
redux-saga is one of an ever-growing number of packages designed to deal with handling side-effects when managing a redux store. It has a more complex API than some alternatives (e.g. redux-thunk), but it is an enormously powerful tool.
This article aims to show you how Firebase’s live database can be integrated with redux-saga to allow real-time updates to your redux store. We will explore one of redux-saga’s more advanced features — eventChannels — to accomplish this.
I’m not going to go through the process of showing how to setup a redux store, or how to connect it to redux-saga — this is explained very well in their respective documentation.
Data Pathways
We are effectively going to be building two data pathways. Firstly, one for sending new data:
This shows a “Created” action that is intercepted by our saga, and sent to the Firebase database. Our reducer will not be affected by the action as the live update from the database will be used to update our reducer.
Secondly, one for receiving data:
In this diagram, the Firebase database sends a live update event, which our saga, via the saga’s eventChannel responds to by sending an “Updated” action. The reducer responds to the update action by adding the new item to its state.
Let’s Write Some Code
Let’s start by defining our action types and action creators — they’re going to be pretty simple — and our reducer.
// You could split these into a constants, actions and reducer file, or not.
export const CREATED = 'CREATE';
export oonst UPDATED = 'UPDATE';
export const create = item => ({
type: CREATED,
payload: {
item
}
};
export const update = item => ({
type: UPDATED,
payload: {
item
}
}
const initialState = [];
export const reducer = (state = initialState, action) => {
switch (action.type) {
case UPDATED:
const newState = state.slice();
newState.push(action.payload.item);
return newState;
default:
return state;
}
};
Hopefully this is all reasonably familiar to you if you have some redux experience.
Sending to the database
Now let’s have some saga fun. We’ll deal with the simpler case of sending a newly created item to the database before moving on to responding to database events.
First let’s import the Firebase bits that we need — in an actual app this would likely be in a separate module so you can re-use it. We’ll also the CREATED action that we want to intercept:
import { CREATED } from '/path/to/itemDuck';
import * as firebase from 'firebase';
const config = {
// Your firebase config
};
// initialize firebase app with our apikey etc.
const app = firebase.initializeApp(config);
// create a firebase database object
const database = firebase.database();
// create a function that inserts an item into the database
function insert(item) {
const newItemRef = database.ref('items').push();
return newItemRef.set(item);
}
It is worth noting that we are returning the result of calling newItemRef.set from our insert function. The set call returns a promise that will resolve on successful insertion, or reject if insertion fails. We can wait for the promise to resolve in our saga
Now we can create our saga:
import { take, call } from 'redux-saga/effects';
function* createItemSaga() {
const action = yield take(CREATED);
const { item } = action.payload;
try {
yield call(insert, item);
} catch (e) {
// do something with the error, such as dispatching an error action with yield put
}
}
This saga takes every CREATED action that is dispatched, retrieves the item from its payload, and pushes it to the server. You can hopefully see how easy error-handling will be with this approach.
To complete our item creation saga, we need to export a saga for our saga middleware to run. We’re going to create another saga in just a second for receiving items, so let’s make a root saga and fork from it so we can run both creating and receiving simultaneously. We’ll fork our receiving saga from the same rootSaga once we’ve written it.
export default function* rootSaga() {
yield fork(createItemSaga);
}
Receiving New Items
So far we haven’t done anything complex — if you have used redux-saga before then it should be very familiar. Let’s move on an write our item receiving saga which will be a little more difficult.
First, let’s import the pieces we’ll need:
import { put } from 'redux-saga/effects';
import { eventChannel } from 'redux-saga';
import { update } from '/path/to/itemDuck';
The Firebase data reference has a useful “on” function, that allows us to add event listeners that are fired when the referenced part of the database changes. We need to find a way to connect this eventListener to our saga… and we can do this with an eventChannel! If you haven’t come across the concept before then there is a great section in the redux-saga docs — they essentially convert events from a listener into something resembling an action that our saga can use. So let’s dive in and write a function to create ourselves an eventChannel.
function createEventChannel() {
const listener = eventChannel(
emit => {
database.ref('items')
.on(
'child_added',
data => emit(data.val()
);
return () => database.ref('items').off(listener);
}
);
return listener;
};
Breaking this function down we are simply calling the eventChannel function with a single argument (a subscriber), and returning the result.
The subscriber listens for the ‘child_added’ event that is fired by the Firebase ref whenever a new item is added, and emits the child to the eventChannel. The return value of the subscriber must be an unsubscribe function so that the listener can be removed if the eventChannel is no longer in use.
We can now go ahead and write the saga that will use this eventChannel.
function* updatedItemSaga() {
const updateChannel = createEventChannel();
while(true) {
const item = yield take(updateChannel);
yield put(updated(item));
}
}
We first create our eventChannel using the function we just wrote. Now we have what looks like an infinite loop — but in the world of generator functions is not — execution will pause at the yield take statement until the updateChannel emits. When we use while(true), what me mean is that we want this saga to run indefinitely.
Similarly to using yield take() on an action, using it on an actionChannel allows us to extract the data that was emitted; in this case our new item. We use a yield put() call to dispatch an update action that our reducer will respond to.
Finally, we just need to add the updatedItemSaga to our root saga, so it now becomes:
export default function* rootSaga() {
yield fork(createItemSaga);
yield fork(updatedItemSaga);
}
Wrapping Up
Our final sagas file looks like this:
import * as firebase from 'firebase';
import { CREATED, update } from '/path/to/itemDuck';
import { put, take, call } from 'redux-saga/effects';
import { eventChannel } from 'redux-saga';
const config = {
// Your firebase config
};
const app = firebase.initializeApp(config);
const database = firebase.database();
function insert(item) {
const newItemRef = database.ref('items').push();
return newItemRef.set(item);
}
function createEventChannel() {
const listener = eventChannel(
emit => {
database.ref('items')
.on('child_added', data => emit(data.val());
return () => database.ref('items').off(listener);
}
);
return listener;
};
function* updatedItemSaga() {
const updateChannel = createEventChannel();
while(true) {
const item = yield take(updateChannel);
yield put(updated(item));
}
}
function* createItemSaga() {
const action = yield take(CREATED);
const { item } = action.payload;
try {
yield call(insert, item);
} catch (e) {
// do something with the error, such as dispatching an error action with yield put
}
}
export default function* rootSaga() {
yield fork(createItemSaga);
yield fork(updatedItemSaga);
}
I hope this was a helpful insight into how Firebase and redux-saga can fit together and allow you to create some really cool features. A live updating database means we can create some real-time features, like chat rooms and games with minimal effort.
Subscribe to Developing Thoughts
Get the latest posts delivered right to your inbox