React authentication tutorial with Firebase V9 and Firestore

vcnsiqueira

VinΓ­cius Siqueira

Posted on May 29, 2022

React authentication tutorial with Firebase V9 and Firestore

In this tutorial we are going to understand how to use Firebase V9 to both setting up the authentication for your application and use the Firestore database to manage additional information about the users.

First things first, if you are reading this, you probably know what Firebase is. For those who does not, Firebase is a Backend-as-a-service platform that provides several tools to developers, like authentication, database, storage, hosting, test lab, notification, among others. It is maintained by Google and it is a very useful platform where you can develop scalable projects.

Now that we already know what Firebase is, let's start our React application. In order to do that we will use the create react app boilerplate. So, move to the folder you want and type the following in your terminal

npx create-react-app authentication
Enter fullscreen mode Exit fullscreen mode

Once the creation is finished, go to the project folder an type

npm start
Enter fullscreen mode Exit fullscreen mode

which, after running, is going to show you the famous React first page in your browser.

React first page

Planning

Okay! Now, let's talk a little about what we are going to create. I always like to plan every project and I suggest every reader to do the same. I encourage you to do that because I think it makes you more focused on what you really have to do. We can always code some components out of the blue, but if you are not focused on what you are doing, it is easy to waste a lot of time. Well, since authentication is the main purpose of this small project, it is a good idea to think about 3 different views:

  1. Login view. We can assume that this is the first page of our app, when people arrive after type the url in the browser. This is going to be the view where the user can type your credentials to possibly access the home page of the application. As credentials we can consider the e-mail and password. So, this view will have a form with both e-mail and password inputs. After filling up both inputs, if the user is registered in the application he will be authorized to go to the home page. Otherwise, he cannot go further.
  2. Register view. Well, since we are going to admit only registered users to go to the home page, we need to create a view where someone can create his own credentials to access the application. Again, since we are considering e-mail and password as credentials, this view is going to have a form with the desired e-mail and password the user wants to register himself.
  3. Finally, we have the home page. This is going to be a view where only authorized users can access after his credentials are accepted by our application. So, let's suppose the home page is going to have a custom welcome message with the user's e-mail and the date when he registered into the application for the very first time.

Structure of the views

I think this is a good starting point. This is not a very fancy application, so we do not have much different components to deal with and, that's why I'm not going to create a big component tree to our application.

App structure

This image could be a good app structure if you want to create a Form component and a Message Component. I am not going to do it, because I want to keep things simple.

  1. The component root of the project is going to be the App component. This component is going to manage the routes of the application. So, it will be responsible to throw the user to the Login page, Register page or Home page.
  2. Also, I am not going to create a big style for the application, since this is not the focus of this project.

Login Page

We start with the Login Page. As I said earlier, the Login Page will just contain a form with two inputs, one to the e-mail and another to the password. In order to do that, we create a new folder into the src that I will call views and inside of it create the folder Login with the files index.jsx and Login.jsx according to the following image

Views and login folder

Inside index.jsx file we just export the default component from the Login.jsx file.

index.jsx

export { default } from './Login';
Enter fullscreen mode Exit fullscreen mode

and inside Login.jsx we create the Login form.

Login.jsx

import React, { useState } from 'react';

const Login = () => {

  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleEmail = event => {
    setEmail(event.target.value);
  };

  const handlePassword = event => {
    setPassword(event.target.value);
  };

  return (
    <div style={{ textAlign: 'center' }}>
      <div>
        <h3>Login</h3>
      </div>
      <div>
        <input
          value={email}
          onChange={handleEmail}
          placeholder="Type your e-mail"
        />
      </div>
      <div>
        <input
          type="password"
          value={password}
          onChange={handlePassword}
          placeholder="Type your password"
        />
      </div>
      <button>
        Submit
      </button>
      <div style={{ fontSize: '12px' }}>
          Dont't have an account?
          {' '}
          Register <span style={{ color: '#293462', fontWeight: 'bold' }}>here</span>
      </div>
    </div>
  );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode

Basically we create a form with a title where we wrote 'Login' and two inputs to deal with the e-mail and password followed by a submit button which, in the future will carry the function to send the user information to be validated. In the end we put a simple text so, if the user is not registered, he will be able to go to the Register Page. We have used the React hooks to create the states email and password and inside the input we use the onChange event handler with both handleEmail and handlePassword function to the e-mail and password inputs respectively.

Remark: I have used inline css in order to create a very simple style to the component. I will repeat some of these in the future. As I mentioned earlier, the focus here is not the style of the application but the logic itself. I strongly recommend you not to use css inline as I am doing here but instead use css modules or styled components, for example.

Register Page

After that, we create a new folder inside the views called Register with the files index.jsx and Register.jsx. These files will be almost exactly the same from those from Login Page as we can see below.

index.jsx

export { default } from './Register';
Enter fullscreen mode Exit fullscreen mode

Register.jsx

import React, { useState } from 'react';

const Register = () => {

  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleEmail = event => {
    setEmail(event.target.value);
  };

  const handlePassword = event => {
    setPassword(event.target.value);
  };

  return (
    <div style={{ textAlign: 'center' }}>
      <div>
        <h3>Register</h3>
      </div>
      <div>
        <input
          value={email}
          onChange={handleEmail}
          placeholder="Type your e-mail"
        />
      </div>
      <div>
        <input
          type="password"
          value={password}
          onChange={handlePassword}
          placeholder="Type your password"
        />
      </div>
      <button>
        Submit
      </button>
      <div style={{ fontSize: '12px' }}>
          Already have an account?
          {' '}
          Please <span style={{ color: '#293462', fontWeight: 'bold' }}>sign in</span>
      </div>
    </div>
  );
};

export default Register;
Enter fullscreen mode Exit fullscreen mode

The only difference, for now, between Register.jsx and Login.jsx is the title and the message in the end. In Register component, we put the message to the user sign in if he already has an account.

The Home Page

The Home Page is the simplest among the three pages. We start doing de same by creating a new folder named Home inside views with the files index.jsx and Home.jsx.

Adding home folder

The index.jsx will be similar to previous ones.

index.jsx

export { default } from './Home';
Enter fullscreen mode Exit fullscreen mode

The Home.jsx will be super easy. Initially we just create a welcome message to the user. After including the authentication, we can improve it.

Home.jsx

import React from 'react';

const Home = () => {
  return (
    <div style={{ textAlign: 'center' }}>
      <h1>Welcome user!</h1>
      <div>
        If you are here, you are allowed to it!
      </div>
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

Creating the routes for the pages

Now, the Login Page, Register Page and Home Page are created, but if you move to your browser you will not see those pages. That's because the application is still rendering what is inside the App component and we do not change anything there. Well, let's change this. Since the App component will be responsible to manage which page to be rendered, we now need the React Router library to create the specific routes. First, we need to install the react-router-dom library. So, go to your terminal and type

npm i react-router-dom
Enter fullscreen mode Exit fullscreen mode

After the installation is completed, move to the App and change the entire code of it by the following

App.js

import {
  BrowserRouter as Router,
  Routes,
  Route,
} from "react-router-dom";

import Home from './views/Home';
import Login from './views/Login';
import Register from './views/Register';

function App() {
  return (
    <Router>
      <Routes>
        <Route path='/' element={<Login />} />
        <Route path='/register' element={<Register />} />
        <Route path='/home' element={<Home />} />
      </Routes>
    </Router>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

All right! What have we done? Well, actually it's not difficult. The react-router-dom library gives us, out of the blue, the hability to manage routes and, that way, the application know which component must render. In order to to that, we import BrowserRouter as Router, Routes and Route from the library.

We can understand the Router as a container that wraps the whole application and allows us to use routes, then we import all the views we created before and for each of it we create an specific Route inside Routes passing as props the path of the route and the element that should be rendered. In this case, we are passing the route '/' to the Login page, '/register' to the Register page and '/home' to the Home page.

Now, if you move to the browser, you will se the Login page, because the localhost url is the route '/', so the application is rendering the Login page.

Login Page

Now, changing the url in the browser adding '/register' in the end will take us to the Register page

Register Page

and, changing it to '/home' will take us to the Home page

Home Page

Now, almost everything is fine but the links to change from the Login page to the Register page are still not working. Well, how could we make it work? In this case, we will need to use the useNavigate hook provided by the react-router-dom library. Its use is quite similar with the previous hook useHistory, which is not available anymore in React Router v6. We just need to import the useNavigate hook from the react-router-dom

import { useNavigate } from 'react-router-dom
Enter fullscreen mode Exit fullscreen mode

call it inside the respective component

const navigate = useNavigate();
Enter fullscreen mode Exit fullscreen mode

and use it in the span element with the onClick prop.
Remark: I also included the pointer cursor in the styles of the span tag so the mouse cursor is going to show a hand when it passes on the text, which shows the user that the text is clickable.

<span 
  onClick={() => navigate('/')}
  style={{ color: '#293462', fontWeight: 'bold', cursor: 'pointer' }}
>
  sign in
</span>
Enter fullscreen mode Exit fullscreen mode

Making these changes to the Login and Register pages, this is the new code of them.

Login.jsx

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom'

const Login = () => {

  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const navigate = useNavigate();

  const handleEmail = event => {
    setEmail(event.target.value);
  };

  const handlePassword = event => {
    setPassword(event.target.value);
  };

  return (
    <div style={{ textAlign: 'center' }}>
      <div>
        <h3>Login</h3>
      </div>
      <div>
        <input
          value={email}
          onChange={handleEmail}
          placeholder="Type your e-mail"
        />
      </div>
      <div>
        <input
          type="password"
          value={password}
          onChange={handlePassword}
          placeholder="Type your password"
        />
      </div>
      <button>
        Submit
      </button>
      <div style={{ fontSize: '12px' }}>
          Dont't have an account? Register {' '}
          <span 
            onClick={() => navigate('/register')}
            style={{ color: '#293462', fontWeight: 'bold', cursor: 'pointer' }}
          >
            here
          </span>
      </div>
    </div>
  );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode

Register.jsx

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';

const Register = () => {

  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const navigate = useNavigate();

  const handleEmail = event => {
    setEmail(event.target.value);
  };

  const handlePassword = event => {
    setPassword(event.target.value);
  };

  return (
    <div style={{ textAlign: 'center' }}>
      <div>
        <h3>Register</h3>
      </div>
      <div>
        <input
          value={email}
          onChange={handleEmail}
          placeholder="Type your e-mail"
        />
      </div>
      <div>
        <input
          type="password"
          value={password}
          onChange={handlePassword}
          placeholder="Type your password"
        />
      </div>
      <button>
        Submit
      </button>
      <div style={{ fontSize: '12px' }}>
          Already have an account? Please {' '}
          <span 
            onClick={() => navigate('/')}
            style={{ color: '#293462', fontWeight: 'bold', cursor: 'pointer' }}
          >
            sign in
          </span>
      </div>
    </div>
  );
};

export default Register;
Enter fullscreen mode Exit fullscreen mode

and after all that we can now click on the span elements to be redirected to the specific pages.

Preview of the application

Now, there is one thing missing. We can only access the Home Page by typing the corresponding route into the url. Of course, that is not what we want. In the end, after the user is signed in, we want the application to redirect him to the Home page. Someone clever could say that it would be enough to use the useNavigate hook in the login page again associated with the submit button. Something like this

const handleSubmit = (event) => {
  navigate('/home');
};
.
.
.
<button onClick={handleSubmit}>
  Submit
</button>
Enter fullscreen mode Exit fullscreen mode

Well, that will work, but that creates a bitter feeling that both e-mail and password are worthless, right? Our application is receiving these inputs from the user and doing absolutely nothing with it. Actually, with this actual approach, the user does not need to fill out his e-mail and password to access the Home page.

Unauthenticated access

And this is not what we want. As we said before, the Home page should only be accessed by an authenticated user. In the end, the handleSubmit function of the Login page needs to check if the user is already registered, and, if so, allows the access to the Home page. And that's what we are going to do in the next section.

Firebase Authentication and Firestore database

After we finally prepare our application, now we need to deal with the user authentication. As I said earlier, we will use the Google Firebase to do that. So, move to https://firebase.google.com/ in your browser. That's the page you will see

Firebase first page

Now, click the console button in the top right corner of the page (you are going to need a Google account) and Firebase will redirect you to a page where all your projects will be available to be chosen. In that page, we click to add a new project. Then we have three simple steps:

  1. Name the project. I am naming it as Authentication
  2. Choose if you want Google Analytics or not. I am going to say yes;
  3. Choose the Firebase account to the Google Analytics. I'm choosing the default one;

Firebase configuration

After that, your project will be created in Firebase. In the project console, we are going to choose both Authentication and Firestore.

Firebase authentication and firestore cards

First, we click in the Authentication card and after redirection, click in Start, then in e-mail and password authentication and then activate it with the respective toggle. After that, click Save.

authentication configuration

Then, select the Firestore card, click in Create Database, choose if the database will be run in production mode or test mode and select the local of your cloud Firestore and click Activate

Firestore configuration

After that, we move to the home page of the project to register it.

Firebase registration

We are almost there. Now move to the settings of the project, the second icon in the left bar, scroll down and you will find some important keys that you will have to import in your React application. Click to copy all this code.

Firebase configuration keys

Before coming back to the code, let's go to the terminal and install firebase as a dependency to our project

npm install firebase
Enter fullscreen mode Exit fullscreen mode

Once it is finished, let's come back to our code. Inside the src folder, we create a folder called configs and inside of it, create a file called firebase.js

Creating firebase file

We now are going to paste the Firebase configuration inside of this file and make some changes.

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: 'your apiKey here',
  authDomain: 'your authDomain here',
  projectId: 'your projectId here',
  storageBucket: 'your storageBucket here',
  messagingSenderId: 'your messagingSenderId here',
  appId: 'your appId here',
  measurementId: 'your measurementId here',
};

export const firebaseApp = initializeApp(firebaseConfig); // initialize app
export const db = getFirestore(); // this gets the firestore database
Enter fullscreen mode Exit fullscreen mode

As you can see, in the code above, inside each field of the object firebaseConfig you put all your firebase codes.

Attention: If you intend to use git as a version control to your code and make it public so everyone can access it in your github, for example, it is not a good idea to simply paste your firebase code inside this file, because everyone could access your firebase API's. So, if you want to keep your keys protected, it's a good idea to create a .env file in the root of your project, paste these important keys there, include the .env file in your gitignore file and call the keys as React environment variables inside firebase.js file.

Creating .env file

The elements in the .env file should not need the ' ' and you don't need to put comma or semicolon in the end of each line

.env structure example

REACT_APP_API_KEY=AIzaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
REACT_APP_AUTH_DOMAIN=authentication-XXXXX.aaaaaaaaaaaaa
Enter fullscreen mode Exit fullscreen mode

.env file

Remark: Do not forget to include your .env file into your gitignore file.

Now that you have done that, move back to the firebase.js and change the firebase keys using the environment variables.

firebase.js

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: `${process.env.REACT_APP_API_KEY}`,
  authDomain: `${process.env.REACT_APP_AUTH_DOMAIN}`,
  projectId: `${process.env.REACT_APP_PROJECT_ID}`,
  storageBucket: `${process.env.REACT_APP_STORAGE_BUCKET}`,
  messagingSenderId: `${process.env.REACT_APP_MESSAGING_SENDER_ID}`,
  appId: `${process.env.REACT_APP_APP_ID}`,
  measurementId: `${process.env.REACT_APP_MEASUREMENT_ID}`,
};

export const firebaseApp = initializeApp(firebaseConfig); // initialize app
export const db = getFirestore(); // this gets the firestore database
Enter fullscreen mode Exit fullscreen mode

Now, remember that we need to do two different things: register a new user and sign in a user. If we go to Firebase authentication documentation we can find two different functions available from Firebase authentication:

  1. createUserWithEmailAndPassword that receives the parameters auth, email and password
  2. signinWithEmailAndPassword that receives the same three parameters

We will use the first one to register a new user and the second to sign the user in the application. So, let's change the firebase.js file including these functions.

firebase.js

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

import {
  getAuth,
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
} from 'firebase/auth';

const firebaseConfig = {
  apiKey: `${process.env.REACT_APP_API_KEY}`,
  authDomain: `${process.env.REACT_APP_AUTH_DOMAIN}`,
  projectId: `${process.env.REACT_APP_PROJECT_ID}`,
  storageBucket: `${process.env.REACT_APP_STORAGE_BUCKET}`,
  messagingSenderId: `${process.env.REACT_APP_MESSAGING_SENDER_ID}`,
  appId: `${process.env.REACT_APP_APP_ID}`,
  measurementId: `${process.env.REACT_APP_MEASUREMENT_ID}`,
};

export const firebaseApp = initializeApp(firebaseConfig); // initialize app
export const db = getFirestore(); // this gets the firestore database

//### REGISTER USER WITH FIREBASE AUTHENTICATION ###//
export const registerUser = (email, password) => {
  const auth = getAuth();
  return createUserWithEmailAndPassword(auth, email, password);
};

//### LOGIN USER WITH FIREBASE ###//
export const loginUser = (email, password) => {
  const auth = getAuth();
  return signInWithEmailAndPassword(auth, email, password);
};
Enter fullscreen mode Exit fullscreen mode

We just import the functions getAuth, createUserWithEmailAndPassword and signInWithEmailAndPassword from firebase/auth and we create the functions registerUser and loginUser to be imported in the respective components.

First, we move to the Register page import the registerUser function

import { registerUser } from '../../configs/firebase';
Enter fullscreen mode Exit fullscreen mode

from firebase.js and create the handleRegister function.

const handleRegister = () => {
    registerUser(email, password)
      .then((userCredential) => {
        alert('User created successfully!')
      })
      .catch((error) => {
        alert('Something went wrong!')
        const errorCode = error.code;
        console.log(errorCode);
      });
  }
Enter fullscreen mode Exit fullscreen mode

This function uses the createUserWithEmailAndPassword that was originally exported from firebase.js. It is important to notice that this function returns a promise, so if it resolves positively, we are just using the native alert to show the message that the user was created successfully and, otherwise, we send a message that something went wrong. I strongly recommend you to create a specific alert component to show the message to the user, but we are not doing it here. To finish, we have to call this handleRegister into the submit button by calling it on the onClick props.

Register.jsx

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { registerUser } from '../../configs/firebase';

const Register = () => {

  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const navigate = useNavigate();

  const handleEmail = event => {
    setEmail(event.target.value);
  };

  const handlePassword = event => {
    setPassword(event.target.value);
  };

  const handleRegister = () => {
    registerUser(email, password)
      .then((userCredential) => {
        alert('User created successfully!')
      })
      .catch((error) => {
        alert('Something went wrong!')
        const errorCode = error.code;
        console.log(errorCode);
      });
  }

  return (
    <div style={{ textAlign: 'center' }}>
      <div>
        <h3>Register</h3>
      </div>
      <div>
        <input
          value={email}
          onChange={handleEmail}
          placeholder="Type your e-mail"
        />
      </div>
      <div>
        <input
          type="password"
          value={password}
          onChange={handlePassword}
          placeholder="Type your password"
        />
      </div>
      <button onClick={handleRegister}>
        Submit
      </button>
      <div style={{ fontSize: '12px' }}>
          Already have an account? Please {' '}
          <span 
            onClick={() => navigate('/')}
            style={{ color: '#293462', fontWeight: 'bold', cursor: 'pointer' }}
          >
            sign in
          </span>
      </div>
    </div>
  );
};

export default Register;
Enter fullscreen mode Exit fullscreen mode

Now, let's go to the Register page and type an email and password and see what happens

Registering a new user

It seems it is working. But what happened? Well, when the user clicked the Submit button, the application called the handleRegister that called the createUserWithEmailAndPassword and checked if everything was fine and created the user. Now let's see the authentication console in Firebase. If you go there you will realize that this new user was added to the list (now with only one user) of users that have credentials to be signed in.

authentication user

Pretty good! Let's see what happened if we try to register with the same user again. I will keep the console open.

Attempt to register an already registered user

Ah-ha! So, as we can see, if an already registered user try to register again, the promise resolves negatively and since we create the console.log(errorCode) inside the catch function, it show exactly why. In this case, the firebase authentication shows us that the e-mail is already in use so it does not register the user again. I encourage you to submit an empty e-mail and password. It will, again, return an error saying that the e-mail is invalid.

Remark: In real word applications we can use this errorCode to show good messages to the user.

Now you already imagine what we are going to do, huh? Yes, you are right! We now are going to use the loginUser function created in firebase.js to sign in an existing user. In order to do this, we move to Login.jsx file, import the loginUser

import { loginUser } from '../../configs/firebase';
Enter fullscreen mode Exit fullscreen mode

and call it inside the previous created handleSubmit function.

const handleSubmit = () => {
    loginUser(email, password)
      .then((userCredential) => {
        alert('User signed in');
        navigate('/home');
      })
      .catch((error) => {
        alert('Something went wrong!');
        const errorCode = error.code;
        console.log(errorCode);
      });
  };
Enter fullscreen mode Exit fullscreen mode

The full Login.jsx becomes this way.

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom'
import { loginUser } from '../../configs/firebase';

const Login = () => {

  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const navigate = useNavigate();

  const handleEmail = event => {
    setEmail(event.target.value);
  };

  const handlePassword = event => {
    setPassword(event.target.value);
  };

  const handleSubmit = () => {
    loginUser(email, password)
      .then((userCredential) => {
        alert('User signed in');
        navigate('/home');
      })
      .catch((error) => {
        alert('Something went wrong!');
        const errorCode = error.code;
        console.log(errorCode);
      });
  };

  return (
    <div style={{ textAlign: 'center' }}>
      <div>
        <h3>Login</h3>
      </div>
      <div>
        <input
          value={email}
          onChange={handleEmail}
          placeholder="Type your e-mail"
        />
      </div>
      <div>
        <input
          type="password"
          value={password}
          onChange={handlePassword}
          placeholder="Type your password"
        />
      </div>
      <button onClick={handleSubmit}>
        Submit
      </button>
      <div style={{ fontSize: '12px' }}>
          Dont't have an account? Register {' '}
          <span 
            onClick={() => navigate('/register')}
            style={{ color: '#293462', fontWeight: 'bold', cursor: 'pointer' }}
          >
            here
          </span>
      </div>
    </div>
  );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode

Now let's see how it works in the browser.

Login with authorized user

Perfect! So, if you try to sign in with a user that is in the authentication list, the access will be allowed and the user will be redirected to the Home page. That's exactly what we wanted. If the user is not registered, we expect the access will be forbidden.

Login with unauthorized user

Yeah! In this case the access was not allowed and, in the console we see the message "user not found" which is exactly what is happening now.

Authorization

We just talked about authentication. Now it's time to set the authorization of our pages. Remember that we said before. We want that the Home Page to be accessed only if the user is authenticated. Otherwise, the user is redirected to the Login Page. In order to do that, we first need to include a button in the Home Page so the user can logout. First, let's move to the firebase.js file and import the signout from firebase/auth

import {
  getAuth,
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
} from 'firebase/auth';
Enter fullscreen mode Exit fullscreen mode

and create in the end the logoutUser function

//### LOGOUT USER ###//
export const logoutUser = () => {
  const auth = getAuth();
  signOut(auth).then(() => {
    alert('User signed out!');
  }).catch((error) => {
    alert('Something went wrong!');
    const errorCode = error.code;
    console.log(errorCode);
  });
};
Enter fullscreen mode Exit fullscreen mode

The changed firebase.js file becomes

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

import {
  getAuth,
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
} from 'firebase/auth';

const firebaseConfig = {
  apiKey: `${process.env.REACT_APP_API_KEY}`,
  authDomain: `${process.env.REACT_APP_AUTH_DOMAIN}`,
  projectId: `${process.env.REACT_APP_PROJECT_ID}`,
  storageBucket: `${process.env.REACT_APP_STORAGE_BUCKET}`,
  messagingSenderId: `${process.env.REACT_APP_MESSAGING_SENDER_ID}`,
  appId: `${process.env.REACT_APP_APP_ID}`,
  measurementId: `${process.env.REACT_APP_MEASUREMENT_ID}`,
};

export const firebaseApp = initializeApp(firebaseConfig); // initialize app
export const db = getFirestore(); // this gets the firestore database

//### REGISTER USER WITH FIREBASE AUTHENTICATION ###//
export const registerUser = (email, password) => {
  const auth = getAuth();
  return createUserWithEmailAndPassword(auth, email, password);
};

//### LOGIN USER WITH FIREBASE ###//
export const loginUser = (email, password) => {
  const auth = getAuth();
  return signInWithEmailAndPassword(auth, email, password);
};

//### LOGOUT USER ###//
export const logoutUser = () => {
  const auth = getAuth();
  signOut(auth).then(() => {
    alert('User signed out!');
  }).catch((error) => {
    alert('Something went wrong!');
    const errorCode = error.code;
    console.log(errorCode);
  });
};
Enter fullscreen mode Exit fullscreen mode

Now we just import the logoutUser function in the Home Page and call it in the created Logout button

Home.jsx

import React from 'react';

import { logoutUser } from '../../configs/firebase';

const Home = () => {
  return (
    <div style={{ textAlign: 'center' }}>
      <h1>Welcome user!</h1>
      <div>
        If you are here, you are allowed to it!
      </div>
      <button onClick={logoutUser}>
        Logout
      </button>
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

Nothing special so far. We still did not block the Home Page to unauthenticated users, but we are on our way to do it.

Inserting logout function

Well let's create the strategy to authorized and unauthorized pages to our application: the routes '/' and '/register' will be available always and the route '/home' will be available only for authenticated users. Right, but how do we know if a user is authenticated or not?

The Firebase authentication helps us with this task. We just need to use the onAuthStateChanged function. For more information we recommend the Firebase documentation that tells us to define an observer to identify if the user is authenticated or not. We are going to make use of the React Context API to create a global state related to that. I'm assuming that you know how to work with context, but if you don't, I suggest this link where I explain how to use it.

Well, in the src folder, we create a folder called context and inside of it we create the folder AuthContext with the file index.jsx.

src/context/AuthContext/index.jsx

import React, { createContext, useState, useEffect } from 'react';
import { getAuth, onAuthStateChanged } from "firebase/auth";

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {

  const [currentUser, setCurrentUser] = useState(null);

  const auth = getAuth();

  useEffect(() => {
    onAuthStateChanged(auth, (user) => {
      if (user) {
        const uid = user.uid;
        setCurrentUser(uid);
      } else {
        setCurrentUser(null);
      };
    });
  }, []);

  return (
    <AuthContext.Provider value={{ currentUser }}>
      {children}
    </AuthContext.Provider>
  );

};
Enter fullscreen mode Exit fullscreen mode

Well, basically this context is constantly listening if there's any changes with the authentication and storing it in the variable currentUser. So, every time a user is authenticated, the currentUser will be equal to the user id of the Firebase authentication and if no user is authenticated this variable is null.

After creating this context we wrap the AuthProvider around the App component

App.js

import {
  BrowserRouter as Router,
  Routes,
  Route,
} from "react-router-dom";

import { AuthProvider } from './context/AuthContext';

import Home from './views/Home';
import Login from './views/Login';
import Register from './views/Register';

function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path='/' element={<Login />} />
          <Route path='/register' element={<Register />} />
          <Route path='/home' element={<Home />} />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

After this, we can use the user id anywhere we want and that is the information we need to allow the user to access or not the Home page. We will create a new generic component called PrivateRoute which will be inside the newly created components folder inside the src folder

PrivateRoute component folder

The PrivateRoute component will be used to wrap the Home Page route component so if the currentUser exists it will render the Home Page and, otherwise it will throw the user to the Login Page

PrivateRoute.jsx

import React, { useContext } from 'react';
import { Navigate} from 'react-router-dom';
import { AuthContext } from '../../context/AuthContext';

const PrivateRoute = ({ children }) => {

  const { currentUser } = useContext(AuthContext);

  if (!!currentUser) {
    return children
  }
  return <Navigate to='/' />

};

export default PrivateRoute;
Enter fullscreen mode Exit fullscreen mode

and then, we import the PrivateRoute in the App component and wrap the Home Page route.

App.js

import {
  BrowserRouter as Router,
  Routes,
  Route,
} from "react-router-dom";

import { AuthProvider } from './context/AuthContext';

import Home from './views/Home';
import Login from './views/Login';
import Register from './views/Register';
import PrivateRoute from "./components/PrivateRoute";

function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path='/' element={<Login />} />
          <Route path='/register' element={<Register />} />
          <Route path='/home' element={
            <PrivateRoute>
              <Home />
            </PrivateRoute>}
          />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now, if we try to access the home page by the url route, the application will not allow us to do that and the Home Page is only accessed by authenticated users.

Home page only accessed by authorized users

How to use Firestore to store data

Everything is working fine, but what is Firestore doing exactly? So far, nothing. And that's because we didn't call it for anything, actually. Let's change this. You can skip this if you don't want to learn how to store data information with Firestore database. If you are still here, let's recall some initial ideas. We wanted that when users logged in, they would be redirected to the Home Page with a custom welcome message that shows his e-mail and the date when they registered. But, for now, we just have the id of the user who access the Home Page through the AuthContext.

But, think about it. If we could store both the e-mail and the register date when the user register himself in the app with his own id and if we could recover this information inside the Home Page our problems would be solved. And a database is precisely the tool used to do that.

Moving back to the Firebase documentation we can find here how we can add data to Firestore. So we move back to the Register page and import the database db from firebase.js and we import the functions doc, setDoc and Timestamp from firebase/firestore and make a small change in the handleRegister so it can write inside the users collection of Firebase Firestore.

Register.jsx

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { registerUser, db } from '../../configs/firebase';
import { doc, setDoc, Timestamp } from 'firebase/firestore';

const Register = () => {

  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const navigate = useNavigate();

  const handleEmail = event => {
    setEmail(event.target.value);
  };

  const handlePassword = event => {
    setPassword(event.target.value);
  };

  const handleRegister = () => {
    registerUser(email, password)
      .then((userCredential) => {
        const user = userCredential.user
        setDoc(doc(db, 'users', user.uid), {
          email: email,
          registeredAt: Timestamp.fromDate(new Date()),
        });
        alert('User created successfully!')
      })
      .catch((error) => {
        alert('Something went wrong!');
        const errorCode = error.code;
        console.log(errorCode);
      });
  }

  return (
    <div style={{ textAlign: 'center' }}>
      <div>
        <h3>Register</h3>
      </div>
      <div>
        <input
          value={email}
          onChange={handleEmail}
          placeholder="Type your e-mail"
        />
      </div>
      <div>
        <input
          type="password"
          value={password}
          onChange={handlePassword}
          placeholder="Type your password"
        />
      </div>
      <button onClick={handleRegister}>
        Submit
      </button>
      <div style={{ fontSize: '12px' }}>
          Already have an account? Please {' '}
          <span 
            onClick={() => navigate('/')}
            style={{ color: '#293462', fontWeight: 'bold', cursor: 'pointer' }}
          >
            sign in
          </span>
      </div>
    </div>
  );
};

export default Register;
Enter fullscreen mode Exit fullscreen mode

Before try it, move to the Firestore console, access the tab Rules and change the code inside of it to the following (specially if you select the production mode during configuration)

Firestore rules

Now, let's try the application. We move to the Register page and create a new registration.

Image description

So, as you can see, now every time a new user register in the application, the e-mail and date of register is stored in Firestore in the collection users inside a document with the user id, under the fields email and registeredAt respectively. Now, we just need to get the data from Firestore inside the Home Page.

Reading the Firestore documentation we just import db from configs/firebase.js and doc and getDoc from firebase/firestore and use the useEffect hook to get this information from firestore every time any change happens in the component. We also import the AuthContext hook to get the user id to access the corresponding document in firestore. So we change the Home Page component this way

Home.jsx

import React, { useContext, useEffect, useState } from 'react';

import { logoutUser, db } from '../../configs/firebase';
import { doc, getDoc } from 'firebase/firestore';
import { AuthContext } from '../../context/AuthContext';

const Home = () => {

  const { currentUser } = useContext(AuthContext);

  const [email, setEmail] = useState(null);
  const [registered, setRegistered] = useState(null);

  useEffect(() => {
    const getUserInformation = async () => {
      const docRef = doc(db, "users", currentUser);
      const docSnap = await getDoc(docRef);

      if (docSnap.exists()) {
        const userData = docSnap.data();
        setEmail(userData.email);
        setRegistered(userData.registeredAt.toDate().toISOString().substring(0,10));
      } else {
        console.log("This document does not exists");
      }
    };

    getUserInformation();
  }, []);

  return (
    <div style={{ textAlign: 'center' }}>
      <h1>Welcome {email}!</h1>
      <div>
        If you are here, you are allowed to it.
      </div>
      <div>
        Date of register: {registered}
      </div>
      <button onClick={logoutUser}>
        Logout
      </button>
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

And now, every time a user access the application, the Home Page will display his e-mail and date of registration.

Home page showing the email and date of registration of the user

Conclusion

It's not too difficult to set up a project with Firebase and use its features (Firestore and Firebase authentication) to handle user authentication and authorization with React!

I hope you enjoy and if you have any questions, just let me know! Thank you all!

πŸ’– πŸ’ͺ πŸ™… 🚩
vcnsiqueira
VinΓ­cius Siqueira

Posted on May 29, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related