Building a SAAS App with React, Redux and Firebase - Part 3

We're continuing our series on building a simple Software-As-A-Service (SAAS) application using React, Redux and Firebase. We're utilizing Firebase as a Backend-As-A-Service (BAAS), and building a simple application that demonstrates some of the core features.

In the last post we wired our applicated up to Firebase Auth and added features for registration, and logging out.

In the post we're going to pick up where we left off by restricting certain routes to Authorized users, and continue to integrate our simple application with Firebase.

NOTE: There are many places where we can improve on the demo application, but the aim of this post is to simply get our feet wet with Firebase. Feel free to Submit a PR on the source repo

The Plan

  • Bugfix to Logout Feature
  • Add Login Feature
  • Add requireAuth Higher Order Component (HOC)
  • Add Auth State to localStorage

Bugfix for Logout Feature

We have our logout feature almost working, but we need to make a small change in order to access App's props inside the signOut call.

Open src/components/App.jsx and update the handleLogout method to this:

  handleLogout = () => {
    // Grab history from props for this context
    const { history } = this.props
    firebase.auth().signOut().then(function() {
      history.push('/login')
      alerts.success('Successfully logged out')
    }).catch(function(error) {
      alerts.error(error.message)
    })
  }

If you look at the componentWillMount method, it is automatically updating our local auth state anytime the firebase auth state changes.

So in handleLogout we're grabbing the history prop outside of the asncy signOut() call to firebase. This is so we have the correct this context.

Add Login Feature

We have our registration and logout features ready, let's add the login feature.

Open src/components/App.jsx and add an import statement:

import Login from 'components/auth/Login'

Then add a new route in the render method below /signup:

...
<Route path="/login" component={Login} />
...

Now we can create the Login component.

There's going to be some duplication here, but we won't worry about it too much at this point. This will allow our Sign Up and Login components to change independently.

Create a src/components/auth/Login.jsx file with the following content:

import React from 'react'
import { Message, Grid, Container, Button } from 'semantic-ui-react'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import firebase from 'firebase/app'
import * as actions from 'actions/auth'
import { Formik, Field, Form, ErrorMessage } from 'formik'
import * as Yup from 'yup'
import * as alerts from 'utils/alerts'

const LoginSchema = Yup.object().shape({
  email: Yup.string()
    .email('Invalid email')
    .required('Required'),
  password: Yup.string()
    .min(6, 'Too Short!')
    .max(50, 'Too Long!')
    .required('Required'),
})

class Login extends React.Component {

  handleSubmit = (values, actions) => {
    actions.setSubmitting(true)
    const { email, password } = values
    firebase.auth()
      .signInWithEmailAndPassword(email, password)
      .then(res => {
        this.props.changeAuth(true)
        alerts.success('Successfully logged in!')
        this.props.history.push("/posts")
      })
      .catch(error => {
        this.props.changeAuth(false)
        alerts.error(error.message)
        actions.setSubmitting(false);
        actions.resetForm()
      })
  }

  render() {
    return (
      <Container>
        <Grid centered columns={2}>
          <Grid.Column>
            <Formik
              initialValues={{email: '', password: ''}}
              onSubmit={this.handleSubmit}
              validationSchema={LoginSchema}
              render={({ errors, touched, isSubmitting }) => (
                <>
                  <Message
                    attached
                    header='Log In'
                    content='Fill out the form below to log into your account'
                  />
                  <Form className="ui form attached fluid segment">
                    <div className='field'>
                      <label>Email</label>
                      <Field type="email" name="email" disabled={isSubmitting} />
                      <ErrorMessage name="email" component="div" />
                    </div>
                    <div className='field'>
                      <label>Password</label>
                      <Field type="password" name="password" disabled={isSubmitting} />
                      <ErrorMessage name="password" component="div" />
                    </div>
                    <Button type='submit' disabled={isSubmitting}>Submit</Button>
                  </Form>
                  <Message attached='bottom' warning>
                    Not signed up? <Link to="/signup">Sign up here</Link> instead.
                  </Message>
                </>
              )}
            />
          </Grid.Column>
        </Grid>
      </Container>
    )
  }
}
const mapStateToProps = ({ auth }) => ({
  auth
})
export default connect(mapStateToProps, actions)(Login)

Again, there is a lot going on here, but it's mostly structure being added for Formik and Semantic-UI-React, validations and the handleSubmit method.
The handleSubmit method submits the data to firebase for processing, and will either Authorize the user OR notify the user of any errors while logging in, then reset the form.

We're manually updating our auth state so our application knows right away that we're logged in. If we let the App component handle it, there is a slight delay while it pings the firebase server. This would cause our system to think we're not Authorized during component mounting.

Test Login/Logout in browser

Fire up your server with yarn start and you should now be able to login with an existing account, and logout without any issues.

After login you should be redirected to /posts, and after logout you should be redirected back to the /login route.

Try using credentials that don't exist to ensure that the invalid user case is handled correctly.

Add requireAuth Higher Order Component (HOC)

We're going to use a Higher Order Component (HOC) to handle restricting routes to Authorized users. Without going into too much detail, a HOC will enhance our component's behavior through composition. These are fairly advanced concepts but let's look at the HOC to get and idea of what's going on.

Create a src/components/requireAuth.js file with the following content:

import React from 'react'
import { connect } from 'react-redux'

export default (ChildComponent) => {
  class ComposedComponent extends React.Component {

    componentDidMount() {
      this.shouldNavigateAway()
    }

    componentDidUpdate() {
      this.shouldNavigateAway()
    }

    shouldNavigateAway() {
      if (!this.props.auth) {
        alerts.error('You must be logged in to access this page.')
        this.props.history.push("/login")
      }
    }

    render() {
      return <ChildComponent {...this.props} />
    }
  }
  const mapStateToProps = (state) => ({
    auth: state.auth
  })

  return connect(mapStateToProps)(ComposedComponent)
}

This Higher Order Component receives a child component as an argument, and connects us to the redux store.

We're using 3 lifecycle methods to determine if the user is authed, then calling shouldNavigateAway() if the user is not authed.

Now let's update our Posts component to restrict it to authorized users only.

Open src/components/Posts/index.js and update it's content to the following:

import React from 'react'
import { connect } from 'react-redux'
import requireAuth from 'components/requireAuth'

class Posts extends React.Component {
  render() {
    return (
      <div>this is posts index</div>
    )
  }
}

export default connect(null)(requireAuth(Posts))

Here we're connecting the Posts component to redux and pass Posts as an argument to requireAuth. This will give the Posts component all the functionality that requireAuth provides and redirect the user away from the current route if they are not authorized.

Test require auth in browser

Now it seems like we have everything in order, so let's fire up the server with yarn start and test it out in the browser at http://localhost:3000.

We should be able to login and be access the /posts route.

Now logout, and try to manually access the http://localhost:3000/posts.

You should be redirected to /login and see an error message:

Screen-Shot-2018-11-13-at-2.07.57-PM

Great, everything seems to be working as expected so far!

Add auth to localStorage

You may have noticed while testing the Login feature in the browser that if you refresh the /posts page, the components are rendered before Apps componentWillMount method completed the async call to firebase.

We're going to address this common issue by adding our auth state to localStorage and add another action creator to handle this.

Open src/actions/auth.js and add update the content to this:

import * as types from 'actions/types'

export const changeAuth = (isAuthed) => dispatch => {
  localStorage.setItem('isAuthed', isAuthed)
  dispatch(setAuth(isAuthed))
}

export const setAuth = (isAuthed) => ({
  type: types.SET_AUTH,
  isAuthed: isAuthed
})

We've updated changeAuth to set the local storage item "isAuthed" and dispatch a new setAuth action which will be passed to the reducer.

We'll need to update our reducer next to hold an initial state of the localStorage item and handle the new SET_AUTH action.

Open src/reducers/auth.js update the content to the following:

import { SET_AUTH } from 'actions/types'

const isAuthed = localStorage.getItem('isAuthed') || false

export default (auth = isAuthed, action) => {
  switch (action.type) {
    case SET_AUTH:
      return action.isAuthed
    default:
      return auth
  }
}

Now our changeAuth action creator will update localStorage on every page load. This gives us a temporary value to work with until Firebase can update the remote auth state locally.

Now while logged in, you can re-test the /posts page in the browser. If you hit refresh, you won't be take to the login page with an error, and the page should load like you'd expect.

Wrap Up

Let's wrap this section up there because though it doesn't seem like we've added many features, we've covered some important aspects of the Application.

Here's what we nailed down:

  • Bugfix to Logout Feature
  • Add Login Feature
  • Add requireAuth Higher Order Component (HOC)
  • Add Auth State to localStorage

Now that we have our Auth system in place for the most part, in the next section we're going to concentrate on getting our Posts data into the Firestore service provided by firebase.

Thanks for following along and I hope you've found this series useful so far. Follow me on twitter for updates: @tmartin8080

Series:

Source code

https://github.com/devato/react-redux-firebase