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

We're continuing our series on building a simple SAAS application using React, Redux and Firebase a the Backend-As-A-Service (BAAS).

In part 1 we organized our files, installed and configured the necessary packages, and created most of the initial components that will lay the foundation for building the app and integrating with firebase.

Let's continue digging in and seeing how we can tackle some problems while developing some features.

In this part we're going to add the Semantic-UI-React library to speed the development of the application up, and start to flesh out the different pages we'll need.

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

  • Add Semantic-UI-React and Styles
  • Add Application Wide Navbar
  • Add Homepage Content
  • Add Sign Up Feature
  • Add Authorization to Navbar
  • Add Client Side Form Validation
  • Add Logout Feature

Add Semantic-UI-React and Styles

We're going to use Semantic-UI-React to get our app up and running. To read more about Semantic-UI-React visit their website and docs.

Install the packages:

$ yarn add semantic-ui-react semantic-ui-css node-sass

To add the styles, Open src/components/App.jsx and add the app stylesheet below the import Posts statement:

...
import "assets/styles/app.scss"
...

Let's import Semantic-UI stylesheets into our app.scss file.

Create src/assets/styles/app.scss with the following content:

@import '~semantic-ui-css/semantic.min.css';

Now Semantic-UI-React and our sass styleseets are configured application wide and we can start to use some of their great components to layout our site. We won't go into too much detail about the components we use, but feel free to read more in their docs.

In order for the app-wrapper declaration to take effect, we'll need to be sure to add a wrapper section with the class name around our routes in App.jsx.

Open src/components/App.jsx and update the render method with the following:

  render() {
    return (
      <div>
        <section className="app-wrapper">
          <Route exact path="/" component={Home} />
          <Route path="/posts" component={Posts} />
        </section>
      </div>
    )
  }

Add Application Wide Navbar

Our navbar component will be shared between all pages in the system, so we going to create it in a common directory.

Create the file src/components/common/HeaderNav.jsx with the following content:

import React from 'react'
import { Menu } from 'semantic-ui-react'
import { Link } from 'react-router-dom'

export default ({ isAuthed }) => (
  <Menu>
    <Menu.Item name='home'}>
      <Link to="/">Home</Link>
    </Menu.Item>
    <Menu.Menu position='right'>
      <Menu.Item name='signup'>
        <Link to="/signup">Sign Up</Link>
      </Menu.Item>
      <Menu.Item name='login'>
        <Link to="/login">Log In</Link>
      </Menu.Item>
    </Menu.Menu>
  </Menu>
)

We'll expand on this component later but for now we have basic ingredients for our application's navigation.

Next we'll add the HeaderNavcomponent to our App component.

Open src/components/App.jsx and import the HeaderNav:

import HeaderNav from 'components/common/HeaderNav'

And add HeaderNav to the render method:

  render() {
    return (
      <div>
        <HeaderNav />
        <section className="app-wrapper">
          <Route exact path="/" component={Home} />
          <Route path="/posts" component={Posts} />
        </section>
      </div>
    )
  }

Ensure that the HeaderNav component is outside the app-wrapper

Add Homepage content

Our home page isn't really import as we're just using it to differentiate public from private pages. Let's just add a title and some lorem ipsum so we know which page we're on.

Open src/components/Home/index.js and update it with the following content:

import React from 'react'
import { Container } from 'semantic-ui-react'

class Home extends React.Component {
  render() {
    return (
      <Container>
        <h2>Home</h2>
        <div>
          <h3>Lorem Ipsum Dolor</h3>
          <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
          <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Donec elementum ligula eu sapien consequat eleifend.</p>
          <p>Donec nec dolor erat, condimentum sagittis sem. Praesent porttitor porttitor risus, dapibus rutrum ipsum gravida et. Integer lectus nisi, facilisis sit amet eleifend nec, pharetra ut augue. Integer quam nunc, consequat nec egestas ac, volutpat ac nisi.</p>
          <p>Sed consectetur dignissim dignissim. Donec pretium est sit amet ipsum fringilla feugiat. Aliquam erat volutpat. Maecenas scelerisque, orci sit amet cursus tincidunt, libero nisl eleifend tortor, vitae cursus risus mauris vitae nisi. Cras laoreet ultrices ligula eget tempus.</p>
        </div>
      </Container>
    )
  }
}

export default Home

Test it out in the browser

Great! After some more basic config and creating some new components we should have what is starting to look like a useful application.

Start the server up with yarn start and test it out in the browser. If everything went well, you should see a basic home page for our app with a header navbar:

Screen-Shot-2018-11-10-at-1.45.45-PM

Add Sign Up Feature

We're going to organize our auth related components in an auth directory.

Create a new file src/components/auth/SignUp.jsx with the content:

import React from 'react'
import { Grid, Container, Button, Form } from 'semantic-ui-react'
import { connect } from 'react-redux'
import firebase from 'firebase/app'
import * as actions from 'actions/auth'
import * as alerts from 'utils/alerts'

class SignUp extends React.Component {

  handleSubmit = ({ target }) => {
    const { email, password } = target.elements
    firebase.auth()
      .createUserWithEmailAndPassword(email, password)
      .then(res => {
        this.props.changeAuth(true)
        alerts.success('Successfully registered!')
        this.props.history.push("/posts")
      })
      .catch(error => {
        this.props.changeAuth(false)
        alerts.error(error.message)
      })
  }

  render() {
    return (
      <Container>
        <Grid centered columns={2}>
          <Grid.Column>
            <h3>Sign Up</h3>
            <Form onSubmit={this.handleSubmit}>
              <Form.Field>
                <label>Email</label>
                <input name="email" placeholder='Enter email...' />
              </Form.Field>
              <Form.Field>
                <label>Password</label>
                <input name="password" placeholder='Enter password...' />
              </Form.Field>
              <Button type='submit'>Submit</Button>
            </Form>
          </Grid.Column>
        </Grid>
      </Container>
    )
  }
}

const mapStateToProps = ({ auth }) => ({
  auth
})

export default connect(mapStateToProps, actions)(SignUp)

This gives us the form to connect to redux and firebase auth.

There is a lot going on here, but we're basically connecting the component to redux and handling the form.

If you take a close look at the handleSubmit method you'll notice that we're calling firebase's createUserWithEmailAndPassword function and handling the response. Spend some time walking through this component and try to really get a hold of what's happening.

Auth Actions

Next let's create the auth action creators.

Create a src/actions/auth.js file with the following content:

import firebase from 'firebase/app'
import * as types from 'actions/types'

export const changeAuth = (isAuthed) => ({
  type: types.CHANGE_AUTH,
  isAuthed: isAuthed
})

changeAuth is a normal action creator that sends the isAuthed value to the auth reducer.

Alerts system

Before this functionality will work, let's add a simple notification system using react-toastify.

$ yarn add react-toastify

Create a src/utils/alerts.js file with the following content:

import { toast } from 'react-toastify'

export const success = (message) => {
  toast.success(message, {
    position: toast.POSITION.TOP_CENTER
  })
}

export const error = (message) => {
  toast.error(message, {
    position: toast.POSITION.TOP_CENTER
  })
}

export const warn = message => {
  toast.warn(message, {
    position: toast.POSITION.TOP_CENTER
  })
}

export const info = message => {
  toast.info(message, {
    position: toast.POSITION.TOP_CENTER
  })
}

Update the App Component

Open src/components/App.jsx and update it to have this content:

import React from 'react'
import { Route } from 'react-router-dom'
import { connect } from 'react-redux'
import { ToastContainer, Zoom } from 'react-toastify';
import firebase from 'firebase/app'

import Home from 'components/Home'
import Posts from 'components/Posts'
import HeaderNav from 'components/common/HeaderNav'
import SignUp from 'components/auth/SignUp'
import "assets/styles/app.scss"
import * as authActions from 'actions/auth'

class App extends React.Component {

  componentWillMount() {
    firebase.auth().onAuthStateChanged(user => {
      if (user) {
        this.props.changeAuth(true)
      } else {
        this.props.changeAuth(false)
      }
    })
  }

  render() {
    const { auth } = this.props
    return (
      <div>
        <HeaderNav isAuthed={auth} />
        <section className="app-wrapper">
          <Route exact path="/" component={Home} />
          <Route path="/posts" component={Posts} />
          <Route path="/signup" component={SignUp} />
        </section>
        <ToastContainer autoClose={false} transition={Zoom} />
      </div>
    )
  }
}

const mapStateToProps = (state) => ({
  auth: state.auth
})

export default connect(mapStateToProps, authActions)(App)

Here here adding our alerts sytem globally, connecting the component to the redux store and passing in all the auth actions as props.

You'll notice as well that we're dispatching the changeAuth action during the componentWillMount lifecycle method. This will keep our auth state up to date whenever the application is loading.

Update authReducer

Our authReducer has just been a placeholder so far, so let's update it to actually handle the auth slice of state, and our isAuthed payload.

Open src/reducers/auth.js and update it with the following content:

import { CHANGE_AUTH } from 'actions/types'

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

We're getting very close to having our Sign Up feature ready, but before we can test it out, we need to update our Authorization navbar items.

Add Authorization to Navbar

Open src/components/common/HeaderNav.jsx and update the content:

import React from 'react'
import { Menu } from 'semantic-ui-react'
import { Link } from 'react-router-dom'

export default ({ isAuthed, handleLogout }) => (
  <Menu>
    <Menu.Item name='home'>
      <Link to="/">Home</Link>
    </Menu.Item>
    { isAuthed && ( 
      <Menu.Item name='posts'>
        <Link to="/posts">Posts</Link>
      </Menu.Item>
    )}

    <Menu.Menu position='right'>
      { isAuthed ? (
        <Menu.Item name='logout'>
          <Link to="#" onClick={handleLogout}>Log Out</Link>
        </Menu.Item>
      ) : (
        <>
          <Menu.Item name='signup'>
            <Link to="/signup">Sign Up</Link>
          </Menu.Item>
          <Menu.Item name='login'>
            <Link to="/login">Log In</Link>
          </Menu.Item>
        </>
      )}
    </Menu.Menu>
  </Menu>
)

You'll notice we're recieving the isAuthed prop from the App component, along with a handleLogout method. Our App component contains the auth state and will handle the logout method for us. We'll work on the Logout feature soon, but first, let's test this puppy out in the browser.

Test Sign Up in the browser

Hopefully you've been able to follow along with the code updates up to this point. There is alot of boilerplate and configuration that goes into building complete applications and our goal is give our readers an idea of what the process involves, without overwheming detail.

Start your dev server with yarn start and go to http://localhost:3000.

If there are any errors, just follow the messages until you get your app running

You should see the homepage of our app with some navbar items.

Test Sign Up Error

Click on the Sign Up navbar item, and try hitting the submit button without entering any data. You should see an error message which is the response from firebase:

Screen-Shot-2018-11-10-at-1.24.42-PM

Experiement some more with other data in the form to see if everything works as expected.

Test Sign Up Success

Now fill out the form with valid data, and you should see a success alert once it's complete:

Screen-Shot-2018-11-10-at-1.29.13-PM

You can also check your firebase authorization dashboard to verify that the user accounts are being created:

Screen-Shot-2018-11-10-at-1.30.32-PM

Client-Side Validation

The errors are being returned from the firebase servers and this is known as server-side validation.

To prevent the API's being sent before the data is valid, we want to validate the data in the client. This process is known as client-side validation.

We're going to add 2 new libraries to help us manage our forms:

  • formik - minimal react form library.
  • yup - schema validation library.
$ yarn add formik yup

Next we'll refactor our SignUp component to implement these new libraries.

Open src/components/auth/SignUp.jsx and update it's content to the following:

import React from 'react'
import { Message, Grid, Container, Button } from 'semantic-ui-react'
import { Redirect, 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'

// Configure Signup form schema
const SignupSchema = 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 SignUp extends React.Component {

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

  render() {
    return (
      <Container>
        <Grid centered columns={2}>
          <Grid.Column>
            <Formik
              initialValues={{email: '', password: ''}}
              onSubmit={this.handleSubmit}
              validationSchema={SignupSchema}
              render={({ errors, touched, isSubmitting }) => (
                <>
                  <Message
                    attached
                    header='Sign Up'
                    content='Fill out the form below to sign-up for a new 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>
                    Already signed up? <Link to="/login">Login here</Link> instead.
                  </Message>
                </>
              )}
            />
          </Grid.Column>
        </Grid>
      </Container>
    )
  }
}
const mapStateToProps = ({ auth }) => ({
  auth
})
export default connect(mapStateToProps, actions)(SignUp)

This is by far the largest component we have so far but if you take some time to slowly read through it, you'll see that it's not that complicated. We're importing helper components from Formik, and Yup and then configuring the validation schema for the form.

Then, we're using Semantic-UI-React and Formik helper components to structure the form. We could probably do some refactoring here to simplify, but for now let's just move on.

You can read more about Formik in their docs. They provide minimal tools to handle the form complexity, and then get out of your way. I prefer this library over redux-form which feels a bit bloated and convuluted.

Test it in the browser

Start your server with yarn start and navigate to /signup. You should now see a sign up form like the one below:

Screen-Shot-2018-11-10-at-4.25.54-PM

Experiment with different validation scenerios and you should find that the validations are handled in real-time, and we're preventing the form from ever being submitted until the form data is valid.

Add Logout Feature

Let's finish up this Part of the series by adding a logout feature.

You may remember from earlier, we updated the HeaderNav component which checks the isAuthed prop to determine which menu set to display.

It also receives a handleLogout prop from the App component. Let's add the method there.

Open src/components/App.jsx and add the following method:

...

  handleLogout = () => {
    firebase.auth().signOut().then(function() {
      this.props.changeAuth(false)
      alerts.success('Successfully logged out')
    }).catch(function(error) {
      alerts.error(error.message)
    })
  }

...

This will simply call the signOut method on firebase to clear the auth, update the state value then display an alert.

Wrap Up

We've accomplished quite a bit in this section and have connected our application to Firebase to create users. There is still alot to cover, but we'll pick up and continue in the next section.

The purpose of this series is to serve as a boilerplate to help get similar applications off the ground. Here's what we covered in this section:

  • Add Semantic-UI-React and Styles
  • Add Application Wide Navbar
  • Add Homepage Content
  • Add Sign Up Feature
  • Add Authorization to Navbar
  • Add Client Side Form Validation
  • Add Logout Feature

If you have any suggestions on how to improve this application, feel free to leave comments or follow me on twitter: @tmartin8080

Series:

Source code

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