CS 466 - React Native & Firebase

Due: 2019-12-02

Goals

  • Learn the basics of online persistence
  • Learn about one of the data storage options on Firebase

Prerequisites

  1. Accept the assignment on our Github Classroom.
  2. Download the git repository to your local computer.
  3. Update the package.json file with your name and email address.
  4. Update app.json with a new slug to match the practical.
  5. Install the package dependencies with npm install

Assignment

For this practical, we are going to learn how to use Firebase's Firestore database to provide some simple data storage.

For simplicity sake, we will start with the same starter code as we did for the persistence practical. This will give us the basic structure we need and won't require the solution of any of the other outstanding practicals.

Part 1: Setup Firestore on Firebase

Start by going to the Firebase Console. You can sign in with your Google account (or your Middlebury account through Google).

Click 'Create a Project' to start a new project. Call it 'Ghost Hunter'. You can decide whether or not to leave on Google Analytics. We won't need it and I turned it off for my project. Once you have clicked through, it will take a few moments for your project to be created, after which, you will be dumped in the console for your project.

Select 'Database' on the left to see the database options. Firebase offers two different databases: Firestore and Realtime Database. Firestore is the newer option with some more features (and a slightly higher cost, but only if we leave the free tier). Click the 'Create Database' link to create a Firestore database.

A dialog will come up asking you to set up the secure rules for the database. For simplicity, we will start in 'Test Mode'. This opens your database for absolutely everyone to see, but it will make our access a lot easier initially. We will lock things down later.

The next option will be the storage bucket -- this is where your actual database will be hosted. The goal is to pick a location near where your users will be to reduce latency. The northamerica-northeast1 option seems a reasonable choice for us. You will see a warning about 'Cloud Functions' support, but we are using Cloud Functions, so this is fine. This should complete the setup of your database. Keep this page handy -- we will return to it.

Part 2: Installing new dependencies

Firebase offers a wide variety of ways to connect to their services. For React Native, there is an apparently nice library called React Native Firebase that uses the native libraries to access the Firebase services. Unfortunately, it is not supports by Expo, so we have to fall back on the Firebase JavaScript API, which is really meant for web development. It is a little slower, but it still works pretty well, and is good enough for our needs.

Rather than working with the API directly, however, we are going to make use of a pair of libraries that integrate Firebase and Firestore into Redux to make state management easier: react-redux-firebase and redux-firestore.

Both libraries are by the same developer, and our interaction will primarily be with react-redux-firebase -- redux-firestore plays more of a supporting role. The documentation is extensive, but also, it must be admitted, a bit confusing. There are several different ways to do everything, so what to do is not always clear.

Regardless, in order to use all of these things, we need to install a collection of different packages:

expo install redux react-redux firebase react-redux-firebase redux-firestore

Part 3: Set up the store

There is, as you will see, a fair amount of boilerplate that we need in order to set everything up. Some of it should look familiar from the last practical as we are again setting up a Redux store. However, unlike last time, we don't need to write our own actions or reducers (nor will we be dispatching actions directly). Our libraries are handling all of that behind the scenes for us. If we wanted to add our own reducers or actions, we certainly could, but we don't need to at the moment.

Create a new directory called 'store' and add a new file called 'store.js' to it.

Copy this code into 'store.js':

import { createStore, combineReducers } from 'redux';
import firebase from 'firebase/app';
import 'firebase/firestore' ;
import { firestoreReducer, createFirestoreInstance } from 'redux-firestore' ;


import {FIREBASE_KEY, FIREBASE_ID} from '../config.json';


// initialize firebase and set up the firebase store
const firebaseConfig = {
  apiKey: FIREBASE_KEY,
  authDomain: `${FIREBASE_ID}.firebaseapp.com`,
  projectId: FIREBASE_ID
};


// Initialize firebase instance
firebase.initializeApp(firebaseConfig);

// Initialize other services on firebase instance
firebase.firestore();


// Add Firebase to reducers
const rootReducer = combineReducers({
  firestore: firestoreReducer,
});


const initialState = {firestore:{}};

export const store = createStore(rootReducer, initialState); 


export const rrfProps = {
  firebase,
  config: {
    userProfile: 'users',
    useFirestoreForProfile: true 
  },
  dispatch: store.dispatch,
  createFirestoreInstance 
}

There are a couple of things to point out in here.

First, note that we have an import statement with no variable attached:

import 'firebase/firestore' ;

We need this for our connection to the Firestore to work.

Project configuration

Then we have the project configuration:

import {FIREBASE_KEY, FIREBASE_ID} from '../config.json';


// initialize firebase and set up the firebase store
const firebaseConfig = {
  apiKey: FIREBASE_KEY,
  authDomain: `${FIREBASE_ID}.firebaseapp.com`,
  projectId: FIREBASE_ID
};

// Initialize firebase instance
firebase.initializeApp(firebaseConfig);

This is the boilerplate that we need to register as an API user of Firebase. Note the two variables we are getting from config.json. These are variables that are specific to your particular Firebase project. Create a new file called config.json at the top level of your project. This file should look like this:

{
  "FIREBASE_API":"API KEY HERE",
  "FIREBASE_ID":"PROJECT ID HERE"
}

Of course, you want to fill in the actual API key and project ID associated with your Firebase project. Return to the console. Click on the little gear on the left and select 'Project settings'. On this page, you will find the API key (called 'Web API Key') and the project ID. Update config.json with these values.

Ideally, we would exclude config.json from our repository since it contains the API keys. We won't bother in this instance, but if you are going to make your repository public, you shouldn't publish your API keys for the world to see.

Setting up Redux

After initializing Firestore, we get to setting up Redux. Note that we are installing a pre-made reducer to handle our connections to Firebase:

// Add Firebase to reducers
const rootReducer = combineReducers({
  firestore: firestoreReducer,
});


const initialState = {firestore:{}};

export const store = createStore(rootReducer, initialState);

Finally, we export a configuration object we will need to make our firestore connection available throughout the app. The name is an artifact of the documentation -- there is nothing special about the name.

export const rrfProps = {
  firebase,
  config: {
    userProfile: 'users',
    useFirestoreForProfile: true 
  },
  dispatch: store.dispatch,
  createFirestoreInstance 
}

Part 4: Make the store and Firestore available

Now it is time to add our store to App.js.

In App.js, import the store and rrfProps from your new file. You will also need to import a Provider from react-redux (just as we did in the last practical) and a ReduxFirestoreProvider from react-redux-firebase.

In another repeat from the last practical, remove the ghosts state, the addGhost function and the screenProps from App.

You are going to wrap the existing View in both a Provider and a ReduxFirestoreProvider. The end result should look like this:

<Provider store={store} >
  <ReduxFirestoreProvider {...rrfProps}>
    <View style={styles.container}>
      <AppNavigator />
    </View>
  </ReduxFirestoreProvider>
</Provider>

Part 5: Adding ghosts

Go to your RadarScreen component. We no longer are getting the addGhost function from props, so delete the line extracting it.

We are going to use the useFirestore() hook to get access to the connection to the database. Add this to the first line of the component:

const firestore = useFirestore();

You will need to import this hook from react-redux-firebase.

Once we have the firestore instance, the interface largely mirrors the official API.

In this instance, we are just going to make use of the add method to add a new ghost to the collection.

If you follow the link, you will see this example for add:

store.firestore.add({ collection: 'cities' }, { name: 'Some Place' }),

We are not getting firestore from store, but this is the basic structure that we want to use. In this example, they are adding the document { name: 'Some Place' } to the cities collection.

Recall that Firestore uses a NoSQL database based on collections of documents. The database is schema-less, so our documents can be any structure we like (though some commonality between documents is suggested). The act of adding a document to a collection creates the document and, if the collection didn't already exist, the collection as well.

On the press of the button, create a new ghost object with createNewGhost, and then add it to the ghosts collection.

This is finally something you can try out. If you have successfully followed along so far, you can add new ghosts and they will show up in the database panel of the Firebase Console.

I found that I needed to reload the console page or go to another tab and come back to make a new collection appear, but once the collection is in place, you should see new ghosts appear in the console in real time.

Part 6: Displaying the ghosts

If you try to see the new ghosts in the app, the app will immediately crash because we haven't dealt with GhostsScreen yet.

In GhostsScreen, we have been getting the ghosts list from the screenProps. Remove this line.

We need to do two things to get the list of ghosts back. First, we need to connect our state store to a set of documents from Firebase. Then, we need to update GhostsScreen in response to changes to this list.

TO accomplish the first part, we are going to use useFirestoreConnect (from react-redux-firebase). We will pass useFirestoreConnect a list of queries that we want to carry out. In our case, we will just fetch all documents in the ghosts collection. That looks like this:

  useFirestoreConnect([
    { collection: 'ghosts' } 
  ]);

Then we need to make our component aware of changes to the list of ghosts so it can re-render if the list changes. We are going to use the same mechanism that we used in the last practical to listen for changes to the store: setSelector. Recall that setSelector takes a function in which the current state is passed in as an argument, and the particular piece of interest is returned. For our purposes, we want the state's firestore instance, from which we will get the results of our query. That looks like this:

const ghosts = useSelector(state => state.firestore.ordered.ghosts);

Note that this also orders the ghosts.

Aside: Offline persistence

As mentioned in class, one of the benefits of using Firebase is that it also supports offline persistence. If you disconnect from your internet connection, you should still see all of your ghosts, and you can even add new ones. When you resume your connection, your changes will be sent off to the main collection. You can test this by putting your device into "Airplane mode" and adding a bunch of ghosts. They should show up in your list, but not in the Firebase console. As soon as your device goes back on line... all of your new ghosts should pop in the console.

Optional: Clearing ghosts

Our need to remove ghosts is lessened because we can delete them from the Firebase console. However, it might be nice to have that functionality anyway. In this interest of keeping this short, this section is optional.

You can follow all of the steps from the last practical to create the 'Clear' button. I did make one small change to the write-up of the last practical. We should pass ClearButton as a JSX component rather than as a function so React actually recognizes it as a component. This allows us to use hooks inside the component.

The one hitch we have is that clearing an entire collection is not supported as part of the Firebase API (perhaps for obvious reasons). Instead, what you need to do is iterate over the documents, deleting them each individually.

Finishing up

Commit your changes to git and push them back up to GitHub. I will find them there.

Grading

Points Requirement
✓/✗ Firebase/Firestore/Redux are implemented correctly
✓/✗ Ghosts can be added
✓/✗ Ghosts can be removed