loading...
Cover image for Creating an app with react and firebase - part three

Creating an app with react and firebase - part three

aurelkurtula profile image aurel kurtula ・7 min read

Welcome to the third and final part of this series on React and Firebase. Before working through this tutorial, make sure you go through parts one and two. You can clone the project and start following this tutorial by working on the branch named part-two

The state of the application so far is that anyone can read and write the todo items that are stored in the database. Today we'll add authentication to the project so that only authenticated users can read and write content. Then of course, we'll further restrict what content each user can read and write, resulting in each users having their own items.

01. Setting up firebase

We're going to use two methods of authentication. Users will be able to register/login with email and password or through facebook.

The steps to enable these two methods over at firebase are already covered in my tutorial on authentication with firebase - you would only need to follow heading "03. Authentication With Email And Password" and "06. Authentication with Facebook" after which you may get back here.

At this point, you should have enabled the login methods "Email/password" and "Facebook".

Lastly, change the database rules to the following.

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

Those rules make sure that no unauthorised user can read or write content.

From this point forward ... it's code all the way down.

02. Login with facebook

Let's start by setting up facebook authentication. As you've already seen in apps you've used around the web, users click on a link/button and authentication happens through a popup.

Head over at ./src/fire.js. There, we'll initialize FacebookAuthProvider which is made available by the firebase package.

import firebase from 'firebase';
const config = {
   ...
};
const fire = firebase.initializeApp(config)
const facebookProvider = new firebase.auth.FacebookAuthProvider();
export { fire, facebookProvider }

In the sixth line, or the second from the bottom, we initialize the provider, then export it, making it available for any other file to import.

Let's do that at ./src/App.js by adding facebookProvider as follows:

import { fire, facebookProvider } from './fire';

Now let's create an authentication method

authWithFacebook=()=>{
  fire.auth().signInWithPopup(facebookProvider)
    .then((result,error) => {
      if(error){
        console.log('unable to signup with firebase')
      } else {
        this.setState({authenticated: true })
      }
    }) 
}

authWithFacebook is a random name I chose, the authentication magic is inside it. Actually it should be very familiar if you read Introduction to Authentication with Firebase tutorial.

To test that this works, go ahead and add a link inside the rendered menu

render() {
  return (
    <BrowserRouter>  
      ...
        <ul className="menu">
          <li><Link to={'/'}>To do</Link></li>
          <li><Link to={'/completed'}>Completed</Link></li>
          <li onClick={this.authWithFacebook}>Login with Facebook</li>
        </ul>
        ...
    </BrowserRouter>   
  );
}

If authentication is successful we're adding {authenticated: true} to the App component state.

But that's not enough.

As we've already explored in the authentication with firebase tutorial, the auth method gives us the ability to listen to authentication changes

fire.auth().onAuthStateChanged()

We can make use of it inside the componentWillMount "lifecycle" component.

03. Accessing data based on authenticated users

The way this works is that when we click on "Login with Facebook" the authentication popup runs. When successful the App component re-renders. Hence re-running componentWillMount making it the perfect place for us to update the application state upon authentication status change.

At the moment this is the code we have.

componentWillMount(){
  this.itemsRef.on('value', data=> {
    this.setState({
      items: data.val()
    })
  })
}

As it stands it does do the job. If no user is authenticated it will still try to get some data but our firebase database rules will prevent access hence data.val() would return nothing. Then when authenticated, the same code re-requests some data and data.val() returns our todo items.

But this would be a nightmare, with the above configuration every user that signs in would have access to the same data, just as before we added authentication.

We want to store user data in an object which only the user can access. Lets re-write some code:

class App extends Component {
  state = {
    items: {},
    authenticated: false, 
    loading: true
  }
  itemsRef = '';
  componentWillMount(){
    this.removeAuthListener = fire.auth().onAuthStateChanged(user=>{
      if(user){
        this.itemsRef = fire.database().ref(`items/${user.uid}`)
        this.itemsRef.on('value', data => {
          this.setState({
            authenticated: true, 
            items: data.val(),
            loading: false
          })
        })
      } else {
        this.setState({
          authenticated: false,
          loading: false
        })
      }
    })
  }
  ...

We're basically doing the same thing but with a slight modification. The most notable thing is that we are no longer writing to an items object in the database but items/${user.uid}. The uid is provided by onAuthStateChanged().

Also, note that we are changing the value of this.itemsRef from within onAuthStateChanged() so that the user's unique ID is available on the component state level.

Visually we are carving a slot in the non-sql database that looks something like this

{
  "items" : {
    "wINebMADEUPCfbvJUSTINZpvCASE1qVRiI2" : {
      "-L1Of70brslQ_JIg922l" : {
        "completed" : false,
        "item" : "item one"
      }
    }
  }
}

Inside items we have user.uid and inside that we have the user's items. This way each user now has access only to their own data.

04. Login out

As I've already covered in the authenticating with firebase tutorial, logging out is very easy:

logOut=()=>{
  fire.auth().signOut().then((user)=> {
    this.setState({items:null})   
  })
}

Then we simply have a button which fires the above method upon click.

05. UI design

Before we move on to authenticating with email and password, let's build a better UI. We now have all the means to give users better UI based on whether they are logged in or not.

Of course the code is going to be in github, so here's a quick overview

In our initial state of the App component we have a property loading: true, Then in the componentWillMount we set loading: false indicating that no matter what we do next, the component has mounted. Hence, we are able to render conditional code.

render() {
    if (this.state.loading) {
      return (<h3>Loading</h3>)
    }
    return ( ... )
}

If the condition is true, the h3 renders on the page. When that's no longer true, the second return statement runs - rendering the code we always had.

We do the same thing to determine whether a user is logged in or not. We have authenticated boolian in our state, which switches from false to true based on authentication status

At the moment, we are already loading ItemsComponent in part one of this series. We are now going to create another component for the menu. But before we do that, let's write the code we want to return in the App component.

import Menu from './components/Menu';
...
return (
      <BrowserRouter>  
        <div className="wrap">
          <h2>A simple todo app</h2>
          <Menu 
            logOut={this.logOut} 
            authenticated={this.state.authenticated} 
            authWithFacebook={this.authWithFacebook} 
          />

In order to keep the code clean, we moved the links into their own component. Here is what we are doing there.

import React from 'react';
import { Link } from 'react-router-dom';
const Menu = (props) => {
  if(props.authenticated){
    return (
      <ul className="menu">
        <li><Link to={'/'}>To do</Link></li>
        <li><Link to={'/completed'}>Completed</Link></li>
        <li className="logOut"  onClick={ props.logOut }>sign out</li>
      </ul>
    );
  } else {
    return (
        <div className="auth">
          <p className="facebook" onClick={props.authWithFacebook}>
            Facebook
          </p>
          <form>
            <label> Email <input type="email" /> </label>
            <label> Password <input type="password" /> </label>
          </form>
        </div>
    );
  }
}
export default Menu;

Simple, We check if user is authenticated. If no user is authenticated, we render the facebook button (which executes authWithFacebook which we've created above), We also display a form. The end result (with css included, which you're able to get in the repository) looks like this

Authenticating with Email and Password

Lets create an EmailAndPasswordAuthentication in our App Component.

EmailAndPasswordAuthentication=(e)=>{
  e.preventDefault()
  const email = this.emailInput.value;
  const password = this.passwordInput.value;
  fire.auth().fetchProvidersForEmail(email)
    .then(provider => {
      if(provider.length === 0){
        return fire.auth().createUserWithEmailAndPassword(email, password)
      }else if (provider.indexOf("password") === -1) {
        console.log("you already have an account with " + provider[0] )
    } else {
      return fire.auth().signInWithEmailAndPassword(email, password)
    }
    })
}

First we prevent the form from running, then get the form input values. Then we run fetchProvidersForEmail by providing it with the email received. That method checks firebase authentication to see if a user with the provided email exists. We therefore use an if statement to act appropriately. Firstly, we say, if nothing is returned, then create a user, with the email and password provided. If this is the case, if the email is new, then a user is created and automatically logged in.

In the second if statement we check if an array with the element of password doesn't exist! This is how it works, when users sign in with, say, facebook, their email is stored in firebase. So if someone tries to register with the same email address, provider returns ["facebook.com"].

Final condition (else) returns an array of ["password"]. I guess that's how firebase chose to tell us whether a user exists and from which provider.

The form

Remember that the form is located at ./src/components/Menu.js, we render it at ./src/App.js like so

<Menu 
    ...
    emailInput={el => this.emailInput = el}
    passwordInput={el => this.passwordInput = el}
    EmailAndPasswordAuthentication={this.EmailAndPasswordAuthentication}
  />

emailInput and passwordInput will take the element passed to it and attach it to a local variable within the App component (this.emailInput and this.passwordInput) and of course this.EmailAndPasswordAuthentication refers to the method we just created.

Now in ./src/components/Menu.js the form looks like this

return (
    <div className="auth">
      <p className="facebook" onClick={props.authWithFacebook}>Facebook</p>
      <form 
      onSubmit={(event) => {props.EmailAndPasswordAuthentication(event) }} 
      >
        <label>
          Email <input type="email" ref={ props.emailInput} />
        </label>
        <label>
          Password  <input type="password" ref={ props.passwordInput} />
        </label>
        <input type="submit" value="Register/Login" />
      </form>
    </div>
);

ref sort of hands the element to the props. So in the App component, this.emailInput would return the same thing as document.querySelector('input[type="email"]').

Conclusion

That's it. We now are able to sign users in with facebook, or email and password. And with that, this mini-project spanned across three posts is complete. The code from this tutorial is at the same github repository in the branch named part-three.

You can check the previous two posts here and here

Posted on Dec 29 '17 by:

aurelkurtula profile

aurel kurtula

@aurelkurtula

I love JavaScript, reading books, drinking coffee and taking notes.

Discussion

markdown guide