State Persistence with Firestore and Redux



I recently created Battlesim, a website and app for playing historical miniature games. While implementing the state maintenance of this app I used Redux. I started with the intention of saving state between sessions entirely within the browser. Eventually I realized that it would be very convenient for multiple users to connect to the same game and have shared state. Firestore was an obvious choice for solving this use case.

The question became how to integrate client side state management for a single user with realtime state updates between users while persisting the data across sessions. Should I drop Redux entirely and use Firestore to solve all three problems? Should I use Redux and avoid introducing another state management solution such as Firebase? It turns out the two go hand in hand quite well.

The Redux Reducer takes some previous state and an action and outputs new state. This provides a trustworthy functional interface so that your entire app can stay in sync. Firebase takes some state, saves it to a remote data store, and updates all connected clients. Here was my strategy for integrating these two solutions:
  1. App shell
    1. Connects to Firebase listening to any data that it needs
    2. Upon any change to the data it calls the Redux Action for that data
    3. Instantiates the app components based upon the current view
  2. App components
    1. Listens to the Redux Store for updates
    2. Implements the app features based upon that data
    3. Sends any needed data updates using a Redux Action
  3. Reducer
    1. Saves data to either Firebase or localStorage before returning the updated state
The key here is that complex business logic and app implementation happens in the app components. However notice that the apps components have no knowledge of the Firestore. Whether the app persists the data to the Firestore, localStorage, neither, or does something else entirely, the app itself is totally separated. It simply does not care where the data goes or how it gets persisted. It just implements the features of the app based upon the data that it receives.

Below is a simple code example of how we would implement this. Notice how the example component connects to Redux and does not need to be concerned about data persistence offering a nice separation of concerns.

/* -------------------------------- */
/* ---------- App shell ----------- */
/* -------------------------------- */
firebase.firestore().collection('example')
.doc(someId)
.onSnapshot(doc => {
  if (doc.exists) {
    store.dispatch(addDocument(doc.id, doc.data()));
  }
});

/* -------------------------------- */
/* -- Some component of your app -- */
/* -------------------------------- */
store.subscribe(() => {
  store.getState()
});

store.dispatch(someAction());

/* -------------------------------- */
/* ----------- Reducer ------------ */
/* -------------------------------- */
const battle = (state = {}, action) => {
  var newState = { ...state };

  if (action.type !== "ADD_DOCUMENT") {
    firebase.firestore()
    .collection('example')
    .doc(newState.id)
    .set(newState);
  }

  return newState;
}


Notice the action type check in the reducer. This means that the add document action is used to update the Redux store from the Firestore but not meant to update the Firestore from the Redux store. All other actions would be persisted to the Firestore and other connected clients would receive the update.

Comments

Popular Articles

The Vanilla Javascript Component Pattern

The Sunless Citadel: A D&D 5e Session Report

Getting Started with Harp