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

Welcome back to part 2 of this build along! I’m literally starting this one right after finishing the last one, so I’m not going to waste any time on a long, winding introduction. We’re gonna style this app a bit to make it less ugly, adding functionality for login and logout actions, and connect Redux to this app so we have a global app state we can use to store the user’s information in, which we’ll then be able to access throughout the app. Let’s get to it!

Styling

If you remember when we first generated the app in expo, the App.js file contained some pre-defined styles for us that looked like this:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

We’re going to take that and break it out into it’s own folder/file, and export it for use throughout the app. We’re also going to remove the justifyContent field, since that forces the content to be vertically centered in the page, and we want our app components to render top to bottom:

// src/styles/styles.js
import { StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
  },
});

export default styles;

With that done, we can now import and use it in our different files to style the different View components, like so:

// src/components/Home.js
const Home = ({navigation}) => {
  return (
    <View styles={styles.container}>
			// [...]
    </View>
  );
};

And our app looks a bit more normal after the fact:

Not much of a change, but it’s good enough for now. Styling is an endless process, and once our app is populated with more components we can do more thorough style changes. For the time being, we’ve moved it to it’s own file which will make it easier to tweak, adjust, and apply app-wide changes in the future. Not let’s add the Login/Logout functionality, so we can interact with the Firestore DB in all the main ways we need to.

Login Form

As I said before, we’ll be rendering the login form on the same page as the signup form, with a button to toggle between them. So first thing’s first, let’s create the LoginForm.js component file:

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 LogInForm 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.signInWithEmailAndPassword(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'
          }}
        >Login to Your 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 LogInForm;

Functionally this form is very similar to the signup form, the main difference being that we call auth.signInWithEmailAndPassword instead of auth.signUpWithEmailAndPassword to complete the action. Now if we go into our AuthPage.js file, import and render the login form while commenting out the signup form just to check if it works and looks good:

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

const AuthPage = ({navigation}) => {
  return (
    <View>
      {/* <SignUpForm navigation={navigation} /> */}
      <LogInForm navigation={navigation} />
    </View>
  );
}

export default AuthPage;

We’ll see that the form renders to the page correctly, has the error handlers in place and functioning, and returns the correct response upon submission:

(I created a new user for this example because I couldn’t remember the password from my old one)

With this form built, now we can work on adding a toggler that allows us to switch between the login and signup forms depending on which action the user wants to take. For this, we’re going to use a component from react-native-elements called ButtonGroup, and implment it in the AuthPage component:

// src/components/AuthPage.js
import React, { Component } from 'react';
import { View } from 'react-native';
import SignUpForm from './SignUpForm';
import LogInForm from './LogInForm';
import { ButtonGroup } from 'react-native-elements';

class AuthPage extends Component {
  constructor (props) {
    super(props)
    this.state = {
      selectedIndex: 0
    }
    this.updateIndex = this.updateIndex.bind(this)
  }

  updateIndex (selectedIndex) {
    this.setState({selectedIndex})
  }  
  
  render() {
    const buttons = ['Login', 'Signup']
    const { selectedIndex } = this.state;
    const AuthView = () => {
      if (this.state.selectedIndex === 0) {
        return <LogInForm navigation={this.props.navigation} />
      } else if (this.state.selectedIndex === 1) {
        return <SignUpForm navigation={this.props.navigation} />
      }
    };
    return (
      <View>
        <ButtonGroup
          onPress={this.updateIndex}
          selectedIndex={selectedIndex}
          buttons={buttons}
          containerStyle={{height: 40}}
        />
        <AuthView />
    </View>
  );
  }
}

export default AuthPage;

Had to make some major changes here, so I’ll break it down bullet point style:

  • We changed the component to a class component in order to create a local state that we can modify, which contains a selectedIndex value which represents which button of the ButtonGroup is currently selected.
  • We create a component called AuthView which returns either the LogInForm or SignUpForm component, depending on the selectedIndex in the local state.

As a result, we now have an AuthPage with a toggler that shows us just the specific form the user wants to see. Login:

Or signup:

With those done, now we just need to add a Logout button to handle the other side of things:

Logout Button

This one is relatively easy compared to the others. Create a LogOutButton.js component file:

// src/components/LogOutButton.js
import React from 'react';
import { auth } from '../config/firebase';

const LogOutButton = () => {
  return <Button title="Logout" onPress={() => auth.signOut()}></Button>
}

export default LogOutButton;

And import/render it in the Home.js component:

// src/components/Home.js
// [...]
import { auth } from '../config/firebase';

const Home = ({navigation, user, isAuthenticated}) => {
  return (
    <View style={styles.container}>
      <Text>Hello World!</Text>
      <Button title="Login/Signup" onPress={() => navigation.navigate('Auth')}></Button>
      <LogOutButton />
      {console.log(auth.onAuthStateChanged(user => {
        if (user) {
          console.log('User logged in')
        } else {
          console.log("User logged out")
        }
      }))}
    </View>
  );
};

// [...]

I also imported firebase auth to get access to the auth.onAuthStateChanged method, which I’ve used to log different messages to the console depending on the users auth state. The result?

We were logged in, then upon clicking the button it logged us out of firebase. Now we can clean our code up a bit by deleting that auth.onAuthStateChanged call as well as the import from the Home.js file, so it looks like this:

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

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

const mapStateToProps = (state) => ({
  user: state.userReducer.user,
  isAuthenticated: state.userReducer.isAuthenticated
})

export default connect(mapStateToProps)(Home);

With this done, we’re ready to implement Redux so we can store that user data in the global state, so we don’t need to query the database every time we want access to the user!

Adding Redux

If you’re not familiar with Redux, I wrote a series about it previously that covers the broad strokes of what you need to know to use it (part 1, part 2, part 3). I’ll included some broader explanations about what/why I’m doing things here, but for a deeper dive on Redux I recommend you go check out those posts.

First things first, let’s install the packages:

// Terminal
yarn add redux react-redux redux-logger

In the src folder we’re going to create a redux folder to house everything we’ll be doing here, and the relevant files:

Then, in configureStore.js:

// src/redux/store/configureStore.js
import { applyMiddleware, createStore } from 'redux';
import { createLogger } from 'redux-logger';
import rootReducer from '../reducers/index';

const store = createStore(
  rootReducer,
  applyMiddleware(
    createLogger()
  )
);

export { store };

Here we create the store and apply the logger middleware to our store so that every time the store changes we can see it in action and how our code is affecting it. We pass the rootReducer in to the store, which we’ll create next:

// src/redux/reducers/rootReducer.js
import { combineReducers } from 'redux';
import userReducer from '../reducers/userReducer';

const rootReducer = combineReducers({
  userReducer: userReducer
});

export default rootReducer;

Here we import the combineReducers method from Redux, and the userReducer (which we’ll make in a second), creating and exporting the rootReducer which is made by calling combineReducers on an object, which has key-value pairs representing the name of the reducer (as the key) and the reducer in question (as the value) which is imported from another file. With that done, let’s create the user reducer;

// src/redux/reducers/userReducer.js
const initialState = {
  user: undefined,
  isAuthenticated: false
}

const userReducer = (state = initialState, action) => {
  switch (action.type) {
    default:
      return state
  };
};

export default userReducer;

Here we create a simple initialState variable to represent the place the app starts at, set the starting point of the reducer to that state, and set up our switch statement just returns the state (for the time being). With our store and reducers in place, we can move on to actually wrapping our app in the Redux store and see how this works!

// App.js
import React from 'react';
import Navigation from './src/components/Navigation';
import { Provider } from 'react-redux';
import { store } from './src/redux/store/configureStore';

export default function App() {
  return (
    <Provider store={store}>
      <Navigation />
    </Provider>
  );
}

We import the Provider components from Redux, wrap the app with it (in place of the React.Fragment we had before), and import the store we just created, passing it in as props to the Provider. With that done, we’re now ready to access the state and display it!

To begin with, let’s connect our home component so we can access the user data in the Redux store to display it when the user is logged in:

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

const Home = ({navigation, user, isAuthenticated}) => {
  return (
    <View style={styles.container}>
      <Text>Hello World!</Text>
      <Button title="Login/Signup" onPress={() => navigation.navigate('Auth')}></Button>
      {console.log("User info:", user)}
      {console.log("Authenticated:", isAuthenticated)}
    </View>
  );
};

const mapStateToProps = (state) => ({
  user: state.userReducer.user,
  isAuthenticated: state.userReducer.isAuthenticated
})

export default connect(mapStateToProps)(Home);

We create a mapStateToProps function which maps the data from the global state onto the props of the Home component. Then we pass that in to the connect function (which we import from react-redux), connecting it to the Home component. Then we destructure user and isAuthenticated off the props for use in the component. Finally, we log these values to the console, just to make sure everything’s working right. The result?

The user info is being logged to the console! If we change those default values in the userReducer just to see if the change takes place throughout the app:

// src/redux/reducers/userReducer.js
const initialState = {
  user: {
    email: "brandon@email.com",
    name: "Brandon Olin"
  },
  isAuthenticated: false
}

const userReducer = (state = initialState, action) => {
  switch (action.type) {
    default:
      return state
  };
};

export default userReducer;

Then we can see the change take place in the console:

So our Redux store is hooked up and ready to be used throughout our app! Now that that’s done, we want to take the user data we receive in the response from Firestore when we sign up or log in, and assign it to the Redux global state in our app, so we have access it throughout.

Pushing User Data to Redux Store

To start with, we’re going to need to make some action creators in Redux, and tweak the reducer a bit to actually return the data we want. So in actions/user.js

// src/redux/actions/user.js
const logInUser = (user) => ({
  type: 'LOG_IN_USER',
  user
})

const logOutUser = () => ({
  type: 'LOG_OUT_USER'
})

export {logInUser, logOutUser};

We’ll use the logInUser action create for both logging in/signing in, since the action we want to take/data we want to save will be identical. From here we can move on to the userReducer:

// src/redux/reducers/userReducer.js
const initialState = {
  user: undefined,
  isAuthenticated: false
}

const userReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'LOG_IN_USER':
      return {
        user: {
          email: action.user.email,
          uid: action.user.uid
        },
        isAuthenticated: true
      };
    case 'LOG_OUT_USER':
      return initialState
    default:
      return state
  };
};

export default userReducer;

Here we’re using the same reducer as before, we just add cases for the log in and log out actions. We access the relevant information from the user object (which we’ll pass in from the sign up and log in forms on the front end), and return them within a user object which will sit in the Redux store.

We also set the isAuthenticated field to true, so we have an easy way to tell if a user is logged in or not, and we can render different components in our app accordingly. For the log out user action we simply reset the Redux store to the initial state, how it was before they ever logged in in the first place.

With that done, now we can go ahead and connect the log in and sign up forms, as well as the log out button to the Redux store, then dispatch the relevant actions when the form is successfully submitted:

// src/components/LogInForm.js
import { connect } from 'react-redux';
import { logInUser } from '../redux/actions/user';

// [...]

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.signInWithEmailAndPassword(this.state.email, this.state.password)
          .then(res => {
						// The dispatch function is available on props in Redux connected
						// components
            this.props.dispatch(logInUser({
              email: res.user.email,
              uid: res.user.uid
            }))
            this.props.navigation.navigate('Home')
          }).catch(e => {
            console.log(e);
          })
      })
  }
}

// [...]

export default connect()(LogInForm);
// src/components/SignUpForm.js
import { connect } from 'react-redux';
import { getUser } from '../redux/actions/user';

// [...]

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 => {
            this.props.dispatch(getUser({
              email: res.user.email,
              uid: res.user.uid
            }))
            this.props.navigation.navigate('Home')
          }).catch(e => {
            console.log(e);
          })
      })
  }
}

// [...]

export default connect()(SignUpForm);

Since our home component is already connected to the Redux store, use the ternary operator to render the log in button when the user isn’t authenticated, and render the log out button when the user is authenticated, like so:

// src/components/Home.js
const Home = ({navigation, user, isAuthenticated}) => {
  const LogInButton = () => <Button title="Login/Signup" onPress={() => navigation.navigate('Auth')}></Button>
  return (
    <View style={styles.container}>
      {isAuthenticated ? <LogOutButton /> : <LogInButton />}
    </View>
  );
};

Before logging in:

After logging in:

And if we look in the console Redux logger provided us some useful information showing how the Redux state changed over the course of that action:

In the previous state at the top we see what the initial state was. In the action we see our action creator as well as the user data we passed into the reducer when we dispatched the action from the front end. In the next state area we see how the Redux state was changed and now contains our user object with the relevant data, as well as the isAuthenticated field set to true.

Whew! That was a lot to do in one post, but our app’s starting to take shape. There are still a few other things we need to do like persist the Firestore logged in status and the Redux store across sessions (it’d be pretty bad UX if the user had to re-log in every time they closed and opened the app), as well as render these buttons in the navbar up top instead of center screen, and well…build out the rest of the app!

But the cool part is we’re getting very close to the point where this framework we’ve built could easily be saved as is, then used as the basic building blocks for any mobile app requiring user accounts/authorization in the future!

I’ll save those minor fixes for part 3 of this series, then after that we’ll get on to the nitty gritty of building this thing out into a multi-feature, more complex app!

Til then,

-Brandon