Cross Platform Mobile App with Expo, Firestore, and Redux (part 1)

Time for another build-along! This time I’m building a mobile app with React Native using the Expo framework, Google Firebase/Cloud Firestore for the backend, and Redux for local state management. I’m particularly excited about this one, as it’s an application that’s actually going to see some real world use!

It was the final project my friend Alex led during the final two weeks of our coding bootcamp at Le Wagon in Bali. She just started her MBA at MIT, and got approval + a grant to build and launch this project at MIT, with two of the dorms signed on for this pilot project.

Our initial test case will be around 400 people, so it’s going to be very interesting building something that’ll lead to real world actions, especially considering the gravity of the food waste issue here in the US, and its impact on climate.

Normally with these build along projects I try to go through step-by-step explaining every piece along the way, so that even a beginner following along can follow my train of thought and learn the how and why of the build process I use. In this project…that’s going to be pretty difficult! Things here are FAR more complex than anything I’ve built previously, so if you struggle to follow along sorry, but I’m doing my best here!

Without further ado, let’s get to it!

Project Initialization

First thing’s first, lets get this project initialized!

If you don’t already have it you’ll need to install expo itself, then initialize the application:

// Terminal
npm i expo-cli --global
expo init MITCompostingApp

After you run the init command, you’ll be given an option as far as what template you’d like to use for your app:

TypeScript is a great option for those ready to use it, but neither I nor my team on this project are that familiar with it. So I’m just going to the go with a blank project for this one.

Once it finishes you’ll need to navigate into the directory that was created for the app, then start the expo server like so:

// Terminal
cd MITCompostingApp
expo start

Upon doing so you’ll be presented with a web page that looks like this:

If you click “Run in web browser” on the left it’ll open the app up in a new tab, and after opening the console to change to a mobile phone view here’s what we get:

Voila! Our first mobile app. Now that that’s done, let’s get our database set up.

Adding Firebase/Firestore

First, let’s install the relevant packages to get this our backend going. In a new terminal tab, run:

// Terminal
expo install firebase @react-native-community/async-storage

Before we dive any deeper here, we’ll need to create a firebase project on the web, which we can do over at https://console.firebase.google.com/. There are a number of steps here and it’d be a waste of time to screenshot every step, so I’m just going to lay it out here for you bullet-point style:

  • There’s a heading that says “Your firebase projects”, under that click “Add project”
  • Give your project a name, such as “MITCompostingApp”, then click continue
  • Click continue to proceed with google analytics enabled (by default), or turn it off if you don’t want them on
  • Select a google analytics account, then click create project
  • It’ll take some time to set up the database for your project, but once it’s completed you’ll see a confirmation message saying “Your new project is ready”. Once that happens, click continue
  • On the left sidebar click “Cloud Firestore”
  • On the next page, click “Create Database”
  • Click “Start in test mode”, then click next
  • Choose a location for your server, then click “Enable”
  • Once it finishes provisioning the database, click “Project Overview” in the top left corner
  • Back on the home screen you’ll see a heading that says “Get started by adding Firebase to your app”. Click the code brackets below the heading (looks like this: </>) to set this up for a web project
  • Once again you’ll need to give your app a nickname, the same name as before should work fine
  • Leave the checkbox blank for “Also set up firebase hosting for this app”, and click “Register App”
  • We should now be on a step titled “Add Firebase SDK”, which means we’re ready to take some of this code and move it into our app!

With that out of the way, let’s get back to a regular style of writing. We’re going to create some files/folders to hold the firebase code, as well as some other code we’ll be writing later on. To start with, let’s create a folder at the root of the project called src, a subfolder called config, and a file within config called firebase.js, where we’ll begin initializing everything we need:

// src/config/firebase.js
import * as firebase from 'firebase';
import 'firebase/auth';
import 'firebase/firestore';

const firebaseConfig = {
  apiKey: "AIzaSyDTSgKBgw90xee4hxw1zESnoR330Gq7JGg",
  authDomain: "scrapitapp-74f38.firebaseapp.com",
  databaseURL: "<https://scrapitapp-74f38.firebaseio.com>",
  projectId: "scrapitapp-74f38",
  storageBucket: "scrapitapp-74f38.appspot.com",
  messagingSenderId: "645504965265",
  appId: "1:645504965265:web:3131d1ce5e28678e9ebd30",
  measurementId: "G-4MN5L9PSB5"
};

if (firebase.apps.length === 0) {
  firebase.initializeApp(firebaseConfig);
}

const db = firebase.firestore();
const auth = firebase.auth();

export { db, auth };

Normally I’d hide the config information you’re seeing here, but since I only generated this project for demonstration purposes and deleted it immediately after, no one can do anything with the API key.

Now with this completed, our database and auth objects are initialized, exported, and ready to be used where we need them!

With this in place we can work on build forms for users to create a new account and log in to an existing account. But we’ll want said forms to live on a separate page, so with that we’ll have to implement some routing and navigation first. Enter:

React Navigation

To start, let’s install the relevant package:

// Terminal
yarn add @react-navigation/native @react-navigation/stack react-native-elements
expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view

These packages will be used to handle the navigation functionality, and react-native-elements is a UI components library we’ll use to render some buttons in this part, and later to render user input fields for the forms we’ll create.

With this done we can begin building out our navigator/menu system. To do so, let’s create a new folder, /components, in the /src folder, then a Navigation.js file within that:

// src/components/Navigation.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

const Stack = createStackNavigator();

const Navigation = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="Home"
          component={Home}
          options={({navigation}) => ({
            headerTitle: 'Overview',
            headerTitleAlign: 'center'
          })}
        />
        <Stack.Screen
          name="Auth"
          component={AuthPage}
          options={{
            headerTitle: 'Login/Sign Up',
						headerTitleAlign: 'center'
          }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  )
}

export default Navigation;

Each instance of Stack.Screen is essentially a single page of the app. The “route” that it can be located at is in the name prop, the component is the UI component in question we’ll be rendering on that page, and the options field is for additional options we can apply to it, which we’ll be doing more with later.

We’re rendering two separate pages here, Home and AuthPage, so let’s build them!

// src/components/Home.js
import React from 'react';
import { Text, View } from 'react-native';
import { Button } from 'react-native-elements';

const Home = ({navigation}) => {
  return (
    <View>
      <Text>Hello World!</Text>
      <Button title="Login/Signup" onPress={() => navigation.navigate('Auth')}></Button>
    </View>
  );
};

export default Home;

Here we create the component and import the Text and View components from react native, as well as the button component from react native elements. Then we destructure the navigation item frrom the props (which is passed in from React Navigation to each Stack.Screen component). Then, we render a quick Hello World! along with a button that, upon clicking, will take users to the Login/Signup page.

Let’s build that Login/Signup page quick with some dummy text just to see if our routing is working:

// src/components/AuthPage.js
import React from 'react';
import { Text, View } from 'react-native';
import { Button } from 'react-native-elements';

const AuthPage = () => {
  return (
    <View>
      <Text>Auth Page</Text>
    </View>
  );
}

export default AuthPage;

With that done, we can import them into our Navigation component so it actually has pages to Navigate to, then import the Navigation component into App.js to render the Home page in place of the current dummy text, and see what happens!

// src/components/Navigation.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import Home from './Home';
import AuthPage from './AuthPage';

// [...]
// App.js
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Home from './src/components/Home';

export default function App() {
  return (
    <View style={styles.container}>
      <Navigation />
    </View>
  );
}

At this point I spent a solid hour bashing my head against the wall, trying to figure out why the damn navigation component wouldn’t render the home page to the screen. For some reason wrapping the Navigation component in a View component cause a blank screen to appear and not show anything else. I’m not entirely sure why this is the case, but when I replaced the view with a React.Fragment using the shorthand syntax, as below:

// App.js
export default function App() {
  return (
    <>
      <Navigation />
    </>
  );
}

It fixed the problem and now renders to the screen!

Not only that, but when we click the Login/Signup button:

It properly navigates us to the auth page!

Wow, that was a rough way to start the morning. Going to get refill my coffee to tackle this next bit.

And we’re back! Now that we’ve got a separate page for Login/Signup, let’s go ahead and build some components to handle those actions. We’re going to use AuthPage as the page on which the user can either log in or sign up, and create separate components for each of those actions, which the user can view by toggling between them.

Since the user will need to create an account before they can ever log in to said account, let’s tack that one first. We’re also going to need a package to validate the users email address, so in the terminal:

// Terminal
yarn add validator

Then create a SignUpForm.js file in the components folder:

// src/components/SignUpForm.js
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { Button, Input } from 'react-native-elements';
import validator from 'validator';
import { auth } from '../config/firebase';
import * as firebase from 'firebase';
import 'firebase/auth';

class SignUpForm extends Component {
  constructor(props) {
    super(props);

    this.state = {
      email: '',
      password: '',
      emailError: '',
      passwordError: ''
    };

    this.handleEmailChange = this.handleEmailChange.bind(this);
    this.handlePasswordChange = this.handlePasswordChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  };

  handleEmailChange(value) {
    if (this.state.emailError) {
      this.setState({ emailError: '' })
    }
    this.setState({ email: value })
  }

  handlePasswordChange(value) {
    if (this.state.passwordError) {
      this.setState({ passwordError: '' })
    }
    this.setState({ password: value })
  }

  handleSubmit() {
    if (!validator.isEmail(this.state.email)) {
      this.setState({ emailError: 'Email address is invalid!' });
    } else if (this.state.password.length < 8) {
      this.setState({ passwordError: 'Password must be at least 8 characters!' });
    } else {
      firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL)
        .then(() => {
          auth.createUserWithEmailAndPassword(this.state.email, this.state.password)
            .then(res => {
              console.log(res)
              this.props.navigation.navigate('Home')
            }).catch(e => {
              console.log(e);
            })
        })
    }
  }

  render() {
    return (
      <View
        style={{
          padding: 20
        }}
      >
        <Text
          style={{
            textAlign: 'center',
            fontWeight: '600',
            fontSize: '18px',
            color: 'gray'
          }}
        >Create an Account</Text>        
        <Input
          label="Email"
          placeholder="email@address.com"
          onChangeText={this.handleEmailChange}
          errorMessage={this.state.emailError}
          leftIcon={{ type: "material-community-icons", name: "email" }}
        />
        <Input
          label="Password"
          placeholder="Password"
          onChangeText={this.handlePasswordChange}
          secureTextEntry={true}
          errorMessage={this.state.passwordError}
          leftIcon={{ type: "font-awesome-5", name: "key" }}
        />
        <Button
          style={{marginHorizontal: 10}}
          title="Submit"
          onPress={this.handleSubmit}
        ></Button>
      </View>
    );
  }
}

export default SignUpForm;

There’s a stupid number of things going on here, and the code is probably more complex than it needs to be, but I’ll try and break down everything that’s happening bullet point style:

  • We render input fields for email and password, calling onChangeText when the field changes to set the local state to the values contained in those fields. It also clears out the error fields when the text changes, which are explained below.
  • We have a handleSubmit function which is triggered when the submit button is pressed:
    • This function uses the validator package to check if the email is valid on form submission. If it’s not, it sets the emailError field in the local state to an error message, which is rendered below the email input field.
    • If it passes the email check, then it checks if the password is 8 characters minimum, passing an error to the passwordError local state field if not and again, stopping the submit function from proceeding.
    • If it passes both these checks, then we call the firebase auth.createUserWithEmailAndPassword method, passing in the email and password contained in the local state, which will create our user in firebase for us. We’ll do more with the response we receive from that call later, but for the time being we’ll just log it to the console then push us back to the home screen.

With this form built and exported we can import and render it on the AuthPage, passing in navigation as props so we actually can push back to the homepage on successful account creation:

// src/components/AuthPage.js
const AuthPage = ({navigation}) => {
  return (
    <View>
      <SignUpForm navigation={navigation} />
    </View>
  );
}

With this in place, we should see our form being rendered correctly when we click the Login/Signup button:

If we enter and invalid email or password and click submit, we should see an error:

And if we enter both a valid email and password, we should be redirected to the homepage and see a response in the console with our user data:

And when I tried to do this, I got an error!

In Firestore, you need to enable the specific sign in methods you want to allow. So if we go back to Firebase from our project overview screen, on the left sidebar click Authentication, then click the Sign-in Method tab, click Email/Password, click the top Enable toggle button, then click save in the bottom right:

Now when we try to create an account with the same credentials:

We’re correctly logged in, and we receive a response containing the email address of the user we just created!

With that, I think this is a good place to finish this first piece in the series. So far we generated a mobile app, added routing to it, connected our database, created a form to allow users to register a new account, and successfully dispatched that action, creating a new account in the process.

In the next post we’ll tweak our styling a bit so the app is less ugly, add the login form/functionality, and begin adding Redux to our app so we can store the user information we receive in the global app state, to make it more easily accessible throughout the app.

Until then!

-Brandon