Build a React Application from First Principles - Firebase Integration

This is Part 7 of the series - Build a React Application from First Principles.

Get the completed code from the last step on GitHub and follow along!

In our previous article, we learnt how to handle events and use it to obtain user information (email and password) from a form. In this article, we will take a big leap and integrate with Firebase, which will store the user information for us, as well as handle user authentication.

Firebase is a Backend as a Service (BaaS) that provides features such as storage, database, authentication and hosting, all for free. You can implement these features yourself, but it will take a lot of time to do it properly, so services like Firebase allows us to produce a MVP extremely quickly.

So, instead of console.log-ing the credentials, we will send it to an endpoint on Firebase.

Set Up

Go to Firebase and create an account (or use your Google account).

Once you've signed up, you'll be brought to the Overview screen. Click on 'Add Firebase to your web app'

And copy the <script> tags it specifies into our project code.


  ...
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.js"></script>
  <script src="https://www.gstatic.com/firebasejs/3.6.4/firebase.js"></script>
</head>
<body>
  <div id="renderTarget"></div>
  <script>
    var config = {
      apiKey: "AIzaSyAKZQWBbnb5N3Ovkx5afBQuvnyVudSobwo",
      authDomain: "grapevine-84264.firebaseapp.com",
      databaseURL: "https://grapevine-84264.firebaseio.com",
      storageBucket: "grapevine-84264.appspot.com",
      messagingSenderId: "209626311092"
    };
    firebase.initializeApp(config);
  </script>
  <script type="text/babel">
    ...

This provides us with the Firebase API.

Creating User

For accounts and authentication, we'll be using the Auth service. Specifically, we'll use the createUserWithEmailAndPassword method to allow users to sign up with their email and password. It will return a promise with a firebase.User object.

So, instead of just console.log-ing the usernames and passwords, we will pass them to firebase.auth().createUserWithEmailAndPassword.

handleSubmit: function (e) {  
  e.preventDefault();
  firebase.auth().createUserWithEmailAndPassword(this.email.value, this.password.value)
    .then(function(user) {
      console.log('User created successfully!');
      console.log(user);
    })
    .catch(function(error) {
      console.log('Sign Up failed. See details below:');
      console.log(error);
    });
},

If we enter our details again and click submit, it should work!

Ooops...it appears there is an error which says that the authentication method is not allowed.

This is because Firebase follows the Principle of least privilege, whereby we should only enable the minimum set of features that allows us to complete a certain action. So, we need to explicitly allow each authentication we wish to accept.

Go to the Firebase console, click on the Authentication tab on the left, then click on 'Set Up Sign In Method'

And enable the 'Email/Password' option. (Make sure you click 'Save')

Now, let's go back and try again. This time we have successfully created a user!

We don't need to do anything with the user object, since it's all stored on Firebase. We can see the entry on the console, under the 'User' tab.

Reactive UI

After a user register, they are automatically logged in. This is done by storing the user's details inside localStorage. The good thing about storing user data in localStorage is that they persist even after you refresh the page.

The user object looks something like this (some keys truncated to save space):

{
    "uid": "c4AQWLfabbVNRsWtHHvMslf0v193",
    "displayName": null,
    "photoURL": null,
    "email": "dev@brew.com.hk",
    "emailVerified": false,
    "isAnonymous": false,
    "providerData": [{
        "uid": "dev@brew.com.hk",
        "displayName": null,
        "photoURL": null,
        "email": "dev@brew.com.hk",
        "providerId": "password"
    }],
    "apiKey": "AIzaSyAKZQWBbnb5N3Ovkx5afBQuvnyVudSobwo",
    "appName": "[DEFAULT]",
    "authDomain": "grapevine-84264.firebaseapp.com",
    "stsTokenManager": {
        "apiKey": "AIzaSyAKZQWBbnb5N3Ovkx5afBQuvnyVudSobwo",
        "refreshToken": "ADDl5SG3Q4-PIn...",
        "accessToken": "eyJhbGciO...",
        "expirationTime": 1482853135999
    },
    "redirectEventId": null
}

Firebase also provides an API method firebase.auth().onAuthStateChanged that allows us to check if the user is logged in or not.

We can use this to make our UI react to the user's logged in state. If the user is not logged in, there should be a text prompt in the header to encourage the user to register/login, and if they are already logged in, have some text to inform they are logged in.

First, let's take the header out and put it into its own component. Since the header will keep the state of whether the user is logged in or not, we will use React.createClass to create the component.


var Header = React.createClass({
  render: function () {
    return (
      <div>
        <img src="./img/logo.png" />
      </div>
    )
  }
});

var App = (
  <div>
    <Header />
    <RegistrationForm />
    <footer>© 2016 Brew Creative Limited. All rights reserved.</footer>
  </div>
);

Next, we need to specify a state variable called userIsLoggedIn, and the text that is displayed in the header will depend on the value of that state variable.

We should set a default value for this variable when the component is first created. To do this, we need to understand something called the lifecycle methods.

Lifecycle Methods

There are some properties of the specification object (the one passed into React.createClass) whose name have a special meaning. We have already encountered render, but there are also propTypes, getDefaultProps and what are known as lifecycle methods.

Lifecycle methods are functions which will run at different stages of the component's lifecycle. For example, we will be using a lifecycle method called getInitialState to set the initial state of the component. It will be run when the component is initiated.

We will use another lifecycle method componentWillMount to run some code just before the component gets added onto the DOM. It will test whether there is a currently-logged-in user, and update our state accordingly.

There are three events that can occur to a component:

  • Mounting - the component is first added onto the DOM
  • Updating - the data passed onto the component is changed, which may or may not require a re-render
  • Unmounting - the component is removed from the DOM

Each event will trigger different sets of methods, called sequentially after one another. See the diagram below to explore each one.

See the Pen React components lifecycle diagram by Eduardo Bouças (@eduardoboucas) on CodePen.

Implementation

So, we use getInitialState to set the initial state of userIsLoggedIn to false. And just before the component is added onto the page, we use componentWillMount to see if the user is logged in, and update the state accordingly. Lastly, depending on the state of userIsLoggedIn, we will display the corresponding texts.


var Header = React.createClass({
  getInitialState: function () {
    return {
      userIsLoggedIn: false
    };
  },
  componentWillMount: function () {
    firebase.auth().onAuthStateChanged(function(user) {
      this.setState({
        userIsLoggedIn: user ? true : false,
      });
    }.bind(this));
  },
  render: function () {
    return (
      <div>
        <img src="./img/logo.png" />
        {
          this.state.userIsLoggedIn ? <p>You're logged in!</p> : <p>Register or log In below</p>
        }
      </div>
    )
  }
});

Now, when the user is logged in, we see:

And if they're not logged in, we see:

Lifting State Up

Now the header will react to the state of the application, we want the body of the page to react as well. So we can update our <RegistrationForm /> component as we did with the <Header/> component, but that would mean repeating our code, and not keeping things DRY.

So how can we reuse the logic we have written already, and apply it to the page body as well?

Well, we can move our state logic up to an <App/> component, and it can pass down the information of whether the user is logged in down to its children, as props.


var Header = function (props) {
  return (
    <div>
      <img src="./img/logo.png" />
      {
        props.userIsLoggedIn ? <p>You're logged in!</p> : <p>Register or log In below</p>
      }
    </div>
  )
}

var App = React.createClass({
  getInitialState: function () {
    ...
  },
  componentWillMount: function () {
    ...
  },
  render: function () {
    return (
      <div>
        <Header userIsLoggedIn={this.state.userIsLoggedIn} />
        { this.state.userIsLoggedIn ? <p>This is where the posts will go!</p> : <RegistrationForm /> }
        <footer>© 2016 Brew Creative Limited. All rights reserved.</footer>
      </div>
    )
  }
});

ReactDOM.render(<App />, document.getElementById('renderTarget'));

Here, we made the <Header/> component stateless, and lifted the state up to the new <App /> component, by transferring all the methods onto it.

We then passed down the userIsLoggedIn state variable to the <Header/> component using props, which looks like normal HTML attributes - userIsLoggedIn={this.state.userIsLoggedIn}.

Lastly, we use the state of the <App/> component to determine whether the registration form would be displayed, or the list of posts.

This is what it looks like now logged in:

And when not logged in.

Q: When to lift state?

A: In order to share state between components, we should lift where we are storing the state to the highest common ancestor of the components that uses that state, and then passes the state down as props to its descendents.

Finishing touches

We have went through a lot in this article:

  • Create some more new UI
  • Integrated with Firebase
  • Configured our UI to update based on whether the user's status

To finish off, we will create a login form, a logout button, and display errors better when it arises when registering or logging in.

Log In Form

The LogInForm component is almost identical to the RegistrationForm component, except it uses signInWithEmailAndPassword instead of createUserWithEmailAndPassword.


var LogInForm = React.createClass({
  handleSubmit: function (e) {
    e.preventDefault();
    firebase.auth().signInWithEmailAndPassword(this.email.value, this.password.value)
      .then(function(user) {
        // Success
      })
      .catch(function(error) {
        // Failure
      });
  },
  render: function () {
    return (
      [Same as RegistrationForm]
    );
  }
});
Log Out Button

For the LogOutButton, we will simply call the signOut method when a user clicks the button.



var LogOutButton = React.createClass({
  handleLogOut: function() {
    firebase.auth().signOut();
  },
  render: function () {
    return <button onClick={this.handleLogOut}>Log Out</button>
  }
});

And we'll add the logout button to the header.


var Header = function (props) {
  return (
    <div>
      <img src="./img/logo.png" />
      {
        props.userIsLoggedIn ? <div><p>You're logged in!</p><LogOutButton/></div> : <p>Register or log In below</p>
      }
    </div>
  )
}

Lastly, add the login form alongside the registration form.


var App = React.createClass({
  ...
  render: function () {
    return (
      <div>
        <Header userIsLoggedIn={this.state.userIsLoggedIn} />
        { this.state.userIsLoggedIn ? <p>This is where the posts will go!</p> : <div><RegistrationForm /><LogInForm/></div> }
        <footer>© 2016 Brew Creative Limited. All rights reserved.</footer>
      </div>
    )
  }
});
Displaying Errors

In each of the forms, we should display the error message from Firebase if an action failed, and then hide the error message when the user changes the value of the form.

To do this, we will use a state variable errorMessage to store the message.


var RegistrationForm = React.createClass({
  getInitialState: function () {
    return {
      errorMessage: '',
    };
  },
  handleSubmit: function (e) {
    e.preventDefault();
    var self = this;
    firebase.auth().createUserWithEmailAndPassword(this.email.value, this.password.value)
      .then(function(user) {
        console.log('User created successfully!');
        console.log(user);
      })
      .catch(function(error) {
        console.log('Sign Up failed. See details below:');
        console.log(error);
        self.setState({
          errorMessage: error.message,
        });
      });
  },
  handleInputChange: function () {
    this.setState({
      errorMessage: '',
    })
  },
  render: function () {
    return (
      <div>
        <h3>Register</h3>
        <form onSubmit={this.handleSubmit}>
          <label htmlFor="registration-form__input-email">
            Email
<input id="registration-form__input-email" type="email" ref={(function (c) { this.email = c }).bind(this)} onChange={this.handleInputChange} />
          </label><br/>
          <label htmlFor="registration-form__input-password">
            Password
  <input id="registration-form__input-password" type="password" ref={(function (c) { this.password = c }).bind(this)} onChange={this.handleInputChange} />
          </label><br/>
          <input type="submit" />
          <div>{this.state.errorMessage}</div>
        </form>
      </div>
    );
  }
});

Now if you try to create a user without a password, it will display an error message.

Now go do the same for the log in form!

That's a wrap!

Download the code and have a play around with it - try logging in, log out, register a new user etc.

Download the completed code from this article on GitHub!

In our next article, we'll use SystemJS and jspm to make our code more modular! To continue, move on to Part 8 - Modules with SystemJS.

Daniel Li

Full-stack Web Developer in Hong Kong. Founder of Brew.

Hong Kong http://danyll.com