React authentication tutorial with Firebase V9 and Firestore
VinΓcius Siqueira
Posted on May 29, 2022
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
Once the creation is finished, go to the project folder an type
npm start
which, after running, is going to show you the famous React first page in your browser.
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:
- 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.
- 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.
- 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.
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.
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.
- 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.
- 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
Inside index.jsx file we just export the default component from the Login.jsx file.
index.jsx
export { default } from './Login';
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;
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';
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;
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.
The index.jsx will be similar to previous ones.
index.jsx
export { default } from './Home';
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;
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
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;
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.
Now, changing the url in the browser adding '/register' in the end will take us to the Register page
and, changing it to '/home' will take us to the 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
call it inside the respective component
const navigate = useNavigate();
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>
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;
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;
and after all that we can now click on the span elements to be redirected to the specific pages.
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>
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.
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
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:
- Name the project. I am naming it as Authentication
- Choose if you want Google Analytics or not. I am going to say yes;
- Choose the Firebase account to the Google Analytics. I'm choosing the default one;
After that, your project will be created in Firebase. In the project console, we are going to choose both Authentication and Firestore.
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.
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
After that, we move to the home page of the project to register it.
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.
Before coming back to the code, let's go to the terminal and install firebase
as a dependency to our project
npm install firebase
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
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
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.
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
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
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:
-
createUserWithEmailAndPassword
that receives the parametersauth
,email
andpassword
-
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);
};
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';
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);
});
}
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;
Now, let's go to the Register page and type an email and password and see what happens
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.
Pretty good! Let's see what happened if we try to register with the same user again. I will keep the console open.
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';
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);
});
};
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;
Now let's see how it works in the browser.
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.
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';
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);
});
};
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);
});
};
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;
Nothing special so far. We still did not block the Home Page to unauthenticated users, but we are on our way to do it.
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>
);
};
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;
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
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;
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;
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.
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;
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)
Now, let's try the application. We move to the Register page and create a new registration.
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;
And now, every time a user access the application, the Home Page will display his e-mail and date of registration.
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!
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
December 13, 2023