Firebase and Redux Saga can be friends

Back-end, Front-end, mobile, Tools

Firebase and Redux Saga can be friends

Kaio Brito

Kaio Brito

What’s this Firebase thingy?

You might have heard about Firebase, a real-time database that stores data as JSON object and was acquired by Google in October of 2014 and became a unified platform for mobile apps.  This article will tell you a bit more on how to move Firebase listeners from the React component to redux-saga.

Integration inside a React Component

Although you can easily connect Firebase within a React component without redux/redux-saga, I’d suggest avoiding this.

import React from "react";
import firebase from "firebase";
import config from "./config.json";

firebase.initializeApp(config);

class TodoList extends React.Component {
  state = {
    todos: []
  };

  componentDidMount() {
    // #1
    const todoRef = firebase.database().ref("todos");

    // #2
    this.listener = todoRef.on("value", snapshot => {
      // #3
      this.setState({ todos: snapshot.val() || [] });
    });
  }

  componentWillUnmount() {
    // #4
    this.listener.off();
  }

  render() {
    return (
      <ul>
        {this.state.todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
      </ul>
    );
  }
}

The integration with Firebase is simple. First #1, you create a reference to the collection you wanna listen to using the method ref. Then #2 you create a listener using the method on and pass a callback that will be called whenever the collection changes, like in #3. As long as this listener object is allocated in memory the callback will be dispatched. The last but not least #4, you must call the method off when in the componentWillUnmount method otherwise you’ll create a memory leak.

This approach has a few problems. The business logic was implemented inside this component, if you want to access the list of todos somewhere else you will have to fetch it from Firebase again. Also, when the component mounts all the listening action will start over, starting new sockets and using more memory in the user’s device.

Reduxyfing all the things

Note: If you’re not familiar with redux yet, I suggest to take a peek on the basics first.

Let’s create some Action Creators and Reducer to connect with the TodoList. The idea here is to create an action that will update all items of a reducer at once. There is a lighter way to do it by listening to change of the items individually, but this won’t be covered.

export const actionTypes = {
  REFRESH_LIST: "TODOS/LIST_REFRESH"
};

export const actionsCreators = {
  updateList: todos => ({
    type: actionTypes.REFRESH_LIST,
    payload: todos
  })
};

const INITIAL_STATE = fromJS({
  items: {}
});

export default (state = INITIAL_STATE, { type, payload }) => {
  switch (type) {
    case actionTypes.REFRESH_LIST:
      return state.set("items", fromJS(payload));
    default:
      return state;
  }
};

Now we need to connect them with the TodoList component.

class TodoList extends React.Component {
  componentDidMount() {
    this.listener = database.ref("todos").on("value", snapshot => {
      this.props.updateList(snapshot.val() || {});
    });
  }

  componentWillUnmount() {
    this.listener.off();
  }

  render() {
    return (
			<ul>
        {this.props.todos
					.map(todo => <li key={todo.get("id")}>{todo.get("text")}</li>)
        }
      </ul>
		);
  }
}

const mapActions = {
  updateList: actionsCreators.updateList
};

export default connect(
  null,
  mapActions
)(TodoList);

It’s a better now, we can access the todo list outside our component but to get them updated in real-time the TodoList still needs to be mounted and we still have that connecting and disconnecting situation.

Redux-Saga

Note: If you already know redux-saga, you can jump to the next section.

I remember when I was trying to convince my teammate Tony to use redux-saga. He used to say “why can’t we just use promises everywhere? It’s easier and simpler”. He was right, promises are part of JS so you don’t need to install anything else and it’s easier to a newcomer understand what’s happening. Depending on the situation using redux-saga may cause the sensation that you’re trying to kill an ant with a nuclear bomb.Don’t call me. I’ll callback you. I promise! Like everything in the world, promises have pros and cons (Callback Hell and Inversion of Control). One of the best things in redux-saga is how easy it is to manage side-effects. Imagine the situation where the user has authenticated in the app, the app has to store the token, inform the user to the bug tracker, fetch the user information/preferences, start up the firebase listeners and etc. Only using promises:

function authenticateWithEmailAndPassword(email, password) {
	axios.post(“auth/token”, {email, password}).then(response => {
		 axios.defaults.headers.common['Authorization'] = ‘Bearer ${response.data.token}’;
	   axios.get(“me”)
	   .then(userResponse => {
	       bugtracker.setUser(response.data).then(...).catch(this.ohIHateMyself);
	    })
	    .catch(this.whatAmIDoingHere);
	})
	.catch(this.killMe);
}

In a nutshell, the idea of redux-saga is to assign tasks to be triggered whenever some actions are dispatched in the redux store. An action can have a pile with 0 or N tasks and the tasks can be assigned have 1 or N actions. The tasks are Generator functions which allow to “pause” the execution until a promise has been resolved, to pause the function you need to use the keyword yield.

What happens under the hood is when an action is dispatched redux-saga puts all the action tasks in a queue and start to execute them, one by one. If after executed the task is still not done (or it’s paused) it’s put back at the end of the queue and the next task is executed, when all the tasks are complete the saga ends. I created this sample to clarify the flow.

This is how the user authenticated scenario would be handled with redux-saga:

export const actionTypes = {
  FETCH_USER_REQUEST: "USER/FETCH",
  FETCH_USER_REQUEST_ERROR: "USER/FETCH_ERROR",

  USER_CHANGED: "USER/CHANGED",
  LOGOUT: "LOGOUT",
  ACCESS_TOKEN_CHANGED: "ACCESS_TOKEN/CHANGED"
};

export const actionCreators = {
  changeUser: user => ({
    type: actionTypes.USER_CHANGED,
    payload: user
  }),
  logout: () => ({
    type: actionTypes.LOGOUT
  }),
  fetchUser: () => ({
    type: actionTypes.FETCH_USER_REQUEST
  }),
  errorFetchingUser: () => ({
    type: actionTypes.FETCH_USER_REQUEST_ERROR
  })
};
import { takeEvery, call, put } from "redux-saga/effects";
import { BugTracer } from "../../Services";
import { UserRequester } from "../../Requesters";

// $5
function* setBugtracerUser({ payload }) {
  yield call(BugTracer.setUser, payload);
}

// #4
function* fetchUser() {
  try {
    const response = yield call(UserRequester.fetchMe);
    yield put(actionCreators.changeUser(response.data));
  } catch (error) {
    yield put(actionCreators.errorFetchingUser(error));
  }
}

// #3
function* dispatchFetchUser() {
  yield put(actionCreators.fetchUser());
}

// #2
function* onTokenChange({ payload }) {
	axios.defaults.headers.common['Authorization'] = payload;
}

// #1
export default function*() {
  yield takeEvery(actionTypes.USER_CHANGED, setBugtracerUser);
	yield takeEvery(
    [actionTypes.ACCESS_TOKEN_CHANGED, actionTypes.LOGOUT],
    onTokenChange
  );
  yield takeEvery(actionTypes.ACCESS_TOKEN_CHANGED, dispatchFetchUser);
  yield takeEvery(actionTypes.FETCH_USER_REQUEST, fetchUser);
}
  1. We’re assigning tasks to be triggered when an specific action is dispatched;
  2. Update the common request header when either a token has been set of the user has logged out;
  3. Dispatch the action that triggers the fetching user’s data saga;
  4. Send an API request to fetch the user’s data and dispatch the changeUser action with the response;
    1. If the request fails, the action FETCH_USER_REQUEST_ERROR will be dispatched instead.
  5. Pass the user’s data to the bug-tracer.

Time to connect all the things

Now that We know how redux-saga works, it’s time to connect it with Firebase but before we do that let’s think a little bit. To subscribe to Firebase updates we need to keep the listener in memory but a task is a generator function, if we start the listener inside the task it will be deallocated when the function has been executed, we won’t get any update and we will just create a memory leak. Also, Firebase doesn’t accept generators functions as the callback so we couldn’t even update our reducers this way, to make it work we need to use eventChannel.

function* startListener() {
  // #1
  const channel = new eventChannel(emiter => {
    const listener = database.ref("todos").on("value", snapshot => {
      emiter({ data: snapshot.val() || {} });
    });

		// #2
    return () => {
      listener.off();
    };
  });

	// #3
  while (true) {
    const { data } = yield take(channel);
		// #4
    yield put(actionsCreators.updateList(data));
  }
}

export default function* root() {
  yield fork(startListener);
}

The eventChannel creates a channel that will subscribe to an event source (firebase updates). Every time the channels gets an update it will emit a signal to the saga passing the update as parameter so we can get the update and dispatch an action.

  1. Creates an eventChannel and starts the listener;
  2. Return the shutdown method;
  3. Creates a loops to keep the execution in memory;
  4. Pause the task until the channel emits a signal and dispatch an action in the store;

That’s how our component will look like at the end.

class TodoList extends React.Component {
  static defaultProps = {
    todos: {}
  };

  render() {
    return (
        <ul>
          {this.props.todos
            .valueSeq()
            .map(todo => <li key={todo.get("id")}>{todo.get("text")}</li>)}
        </ul>
    );
  }
}

const mapStateToProps = state => {
  return {
    todos: state.todos.get("items")
  };
};

export default connect(mapStateToProps)(TodoList);

Alright, now our component can only focus on render itself, we can access the todos in any part of the app and to keep getting updated we don’t need to have the TodoList mounted.

Conclusion

Separating business logic from the component is an excellent approach, sometimes it requires a bit more work and lines of code but the result is satisfactory with the possibility of code sharing between different platforms, more reusable components and simpler tests. Firebase is a really cool and huge platform, the real-time database is the tip of the iceberg, integrating it with redux-saga make this separation of concerns possible but it’s not easy to do it in the first try, with this article everything should be clear now (well, I hope so).

I created this repo with the source code used in this article.


Related links:

  1. https://firebase.googleblog.com/2014/10/firebase-is-joining-google.html
  2. http://callbackhell.com/
  3. https://redux-saga.js.org/
  4. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
  5. https://frontendmasters.com/courses/rethinking-async-js/callback-problems-inversion-of-control/

Hire Kaio as a speaker?

Hire Kaio as a speaker?
Kaio Brito

Kaio Brito

Ever come across a guy who compares web development to football? If not, meet Kaio, one of Brazil's finest mobile developers. According to coach Kaio, we should focus on fixing things instead of blaming someone for problems. And whenever you win or lose, you do so as a team. Wise words from a true go-getter. Or could he be playing FIFA18 a tad too much with his son?

our blog Related blog articles

Do you have a question? Leave a comment