It would look like this, if you use the same styles as this project. π
π₯ Creating the authentication designs.
In this application we will not handle routes, we only focus on authentication. So the login and register designs can be put in separate views if you like, in my case they will only be components.
I do it this way to explain it in the simplest way.
π₯ Designing the login.
Create the folder src/components and inside create the file Login.tsx.
The login will consist of 2 inputs:
Email.
Password.
The inputs, must have to contain the name attribute identifying each input.
It will also have 2 buttons:
Normal login.
Login with Google.
Each button must have its type property, this is so that the Google button does not post the form, only the first button must do that.
exportconstLogin=()=>{return (<divclassName="container-auth"><h2>Login</h2><form><inputname="email"type="email"placeholder="E-mail"/><inputname="pass"type="password"placeholder="Password"/><divclassName="container-buttons"><buttontype="submit">Log In</button><buttontype="button"> Google </button></div></form></div>)}
π₯ Designing the registration.
The design of the registration will be the same as the login, with the same inputs but only one button.
exportconstRegister=()=>{return (<divclassName="container-auth"><h2>Create an account</h2><form><inputname="email"type="email"placeholder="E-mail"/><inputname="pass"type="password"placeholder="Password"/><divclassName="container-buttons"><buttontype="submit">Sign up</button></div></form></div>)}
Now we create a barrel file, (index.ts) in the src/components folder to be able to export our components and import them elsewhere in a more orderly way.
export*from'./Login'export*from'./Register'
Once we have the two designs and the barrel file, we go to src/App.tsx and add them:
To handle each form, we are going to implement a custom hook, we will call it useForm.tsx and it will be inside the folder src/hooks.
We create a function that receives as parameter an object that contains the initial state of the form and this will have a generic type, this so that it is a little more reusable (in case they want to add more fields to the forms) the hook although in this case it is not necessary since we have the same fields in both forms.
Then, we will use the state to store the values of the form. And with the handleChange function we will be able to handle the changes of the input and store its values.
The handleChange function sends a computed property to the setForm function and that is why we need the name attribute in the input, to identify it and get its value.
Finally we return, by means of the operator rest we spread the values of the form, then the form and finally the function handleChange.
We call the hook useForm we send it an object that will be the initial state of the form, in this case we will have two properties that is the email and pass, that make reference to the inputs that we have in the HTML. Their default values are empty strings.
We destructure the properties of the form and the function handleChange.
In the inputs we place the value attribute with its corresponding value and the onChange attribute sending the handleChange function to manage the input state.
Finally, we will make a function called handleSubmit that receives the form event, and this function for the moment only prevents the default form behavior.
We pass the handleSubmit function to the onSubmit attribute of the form tag.
This is how Login.tsx would look like for the moment
import{useForm}from'../hooks/useForm';exportconstLogin=()=>{const{handleChange,pass,email}=useForm({initialState:{email:'',pass:''}})consthandleSubmit=(e:React.FormEvent<HTMLFormElement>)=>{e.preventDefault()}return (<divclassName="container-auth"><h2>Login</h2><formonSubmit={handleSubmit}><inputname="email"type="email"placeholder="E-mail"onChange={handleChange}value={email}/><inputname="pass"type="password"placeholder="Password"onChange={handleChange}value={pass}/><divclassName="container-buttons"><buttontype="submit">Log In</button><buttontype="button"> Google </button></div></form></div>)}
Just as we did the Login.tsx it is exactly the same in the Register.tsx file.
import{useForm}from"../hooks/useForm"exportconstRegister=()=>{const{handleChange,pass,email}=useForm({initialState:{email:'',pass:''}})consthandleSubmit=(e:React.FormEvent<HTMLFormElement>)=>{e.preventDefault()}return (<divclassName="container-auth"><h2>Create an account</h2><formonSubmit={handleSubmit}><inputname="email"type="email"placeholder="E-mail"onChange={handleChange}value={email}/><inputname="pass"type="password"placeholder="Password"onChange={handleChange}value={pass}/><divclassName="container-buttons"><buttontype="submit">Sign up</button></div></form></div>)}
β οΈ Note: You can even create a single reusable component, as the forms are almost exactly the same only differing by the buttons and the handleSubmit action as this function will do something different depending on the form.
We already have the design and functionality of the forms, we continue with the creation of our app in Firebase.
π₯ Configuring Firebase.
Now we have to configure the application in firebase to be able to use its authentication service.
π₯ Creating the project.
1 - Let's go to the firebase console, we log in with a Gmail account.
2 - If this is the first time you are using Firebase, you will see a message and a create project button which you should press.
3 - Place the new name to the project. In my case I named it auth-firebase-react.
And at the end there is a continue button that you must press.
4 - Wait for your project to finish creating and then click continue.
Once you continue, you will be sent to a new panel.
π₯ Creating the app.
1 - In the new panel, you have to identify these buttons. Press the web button to create an app (the third button with white background).
2 - Name your app and click on Register App.
3 - Once the app is registered, we will be given our credentials, which we have to save because we are going to use them later.
π₯ Configuring the authentication.
1 - Now, we have to go back to the panel, in the menu on the right side there is an option that says compilation and inside there is the option authentication and you have to click on it, so it will take you to another screen.
And you have to click on the Start button.
2 - Then you will be shown several providers, from which we are going to select the email/password and Google (you can choose the ones you want, although you will probably have to do more configuration).
When you choose email/password just click on Enable and at the end there is a save button that you must press once you finish making any modification in the provider.
When you choose Google you will do the same as the previous one, and you will also have to select your email.
3 - Once the providers are enabled, it should appear in the Sign-in method tab as follows.
4 - In the Users tab you can see all the users that are registered in your application.
π₯ Configuring Firebase in our React app.
Back to our React app, let's install Firebase.
npm install firebase
create a new folder src/firebase and inside a file called config.ts and paste all the configuration you gave us in the previous section
In my case, I put the values of each property in an environment variable, only creating in the root of the project a .env file.
Each variable must begin with the word VITE_ for them to work.
VITE_APIKEY=1231231465
# more vars
And to call an environment variable we have to use the import.meta.env['Nombre de la variable']
Note: you should also notice that I changed the name of the app variable to FirebaseApp.
Now to use the Firebase authentication service, we use the getAuth method and we have to get it from 'firebase/auth', then we send it the initialization of our app, that is the FirebaseApp constant.
Now let's create a new file inside src/firebase named services.ts
Note: all the firebase functions that we are going to use come from firebase/auth
π₯ 1 - Creating the function to authenticate by Google.
First we must create a new instance of the provider we have chosen, in this case Google.
Then we create an asynchronous method, and inside a try/catch because either the user makes a mistake or something goes wrong.
Through the signInWithPopup method, we have to send it our instance of FirebaseAuth, which we had already created in the previous section, and the provider instance.
If everything goes correctly, from the user property of the result variable, it will give you several information as you can see in the destructuring, but we are only going to use the uid that's why we return it.
And in the catch, in fact in all the catch of this file, we are only going to send an alert with the message that Firebase provides us.
π₯ 2 - Creating the function to authenticate by credentials.
The function for login and register using credentials are the same, they only differ in the method.
Both receive an object containing the email and password and if all goes well, return the uid (these functions also return the same as the google authenticate, such as displayName, photoURL, etc.).
Both the createUserWithEmailAndPassword and signInWithEmailAndPassword functions receive the FirebaseAuth instance, and an email and password.
createUserWithEmailAndPassword, creates the user in Firebase.
signInWithEmailAndPassword, verifies if the user exists in Firebase.
So how do we solve this problem, well by observing the authentication status of the user.
For this we need a couple of things.
First create a function, which is going to receive as parameter a callback, that is a function, this function will help us to establish the authenticated or unauthenticated user.
You can notice in the code that we will use a setter of useState and that we will also use the Context API, the typing will fail because we have not created the context yet, so for the moment you can set the type to any.
But what is important now is that we receive the function setSession.
// type StateDispatch = React.Dispatch<React.SetStateAction<Pick<AuthStateContext, "status" | "userId">>>typeStateDispatch=anyexportconstonAuthStateHasChanged=(setSession:StateDispatch)=>{}
Now we will use the function onAuthStateChanged, which receives as first parameter the FirebaseAuth.
import{onAuthStateChanged}from'firebase/auth'import{FirebaseAuth}from'./config'// type StateDispatch = React.Dispatch<React.SetStateAction<Pick<AuthStateContext, "status" | "userId">>>typeStateDispatch=anyexportconstonAuthStateHasChanged=(setSession:StateDispatch)=>{onAuthStateChanged(FirebaseAuth)}
The second parameter is a callback, which returns the user if his active session exists, otherwise it returns undefined.
import{onAuthStateChanged}from'firebase/auth'import{FirebaseAuth}from'./config'// type StateDispatch = React.Dispatch<React.SetStateAction<Pick<AuthStateContext, "status" | "userId">>>typeStateDispatch=anyexportconstonAuthStateHasChanged=(setSession:StateDispatch)=>{onAuthStateChanged(FirebaseAuth,user=>{})}
We evaluate the user:
If it does not exist, we use the setSession to set the status to no-authenticated and the user id to null. (don't forget to set the return to avoid executing the following line)
If it exists, we use the setSession to set the status to authenticated and the user id.
import{onAuthStateChanged}from'firebase/auth'import{FirebaseAuth}from'./config'// type StateDispatch = React.Dispatch<React.SetStateAction<Pick<AuthStateContext, "status" | "userId">>>typeStateDispatch=anyexportconstonAuthStateHasChanged=(setSession:StateDispatch)=>{onAuthStateChanged(FirebaseAuth,user=>{if (!user)returnsetSession({status:'no-authenticated',userId:null})setSession({status:'authenticated',userId:user!.uid})})}
You probably don't understand why we send status or userId, well that's the data we will need in our global status, when we are going to create the context of our app.
π₯ 4 - Creating the function to logout.
Now what happens, thanks to the fact that we are observing the user's authentication status, we can't change the user for a long time, even if you reload or close the browser.
Well for that, we must log out, and it's very simple:
Then we are going to create the initial status of our context.
By default the status will be checking because at the beginning we don't know if the user is authenticated or not, we will know it once certain functions are executed. And also the userId will be null by default until we check the authentication status of the user.
Now we are going to use the function we created before to observe the user's authentication status.
We will do it in an effect that should only be executed the first time the application starts. And the callback that we will send it will be the setSession.
Then we will make the function that executes the logout. We call the logoutFirebase function and set the session with the userId to null and the status to no-authenticated.
Next, we will make a function that we will reuse in other functions, since we want to avoid repeating so much code.
This function receives the userId that can be a string or undefined, we evaluate if the userId is a string:
If the userId is a string, it means that the user is authenticated and we set the session with the userId and the status to authenticated. (don't forget to set the return to avoid executing the next line).
If the userId is undefined, we call the handleLogOut function, because the user does not have a valid authentication, and we need to close all sessions.
The following functions, as well as this one will be similar, first do the checking, then execute the specific function for this task which returns the userId or undefined and call the validateAuth sending what returns this function
Now we need to wrap our app with the provider. To do this we go to the highest point of our app which is the src/main.tsx file and add the AuthProvider
Now we go to the src/components/Register.tsx file and use our context as follows:
We import the useContext hook and send the AuthContext and get the handleRegisterWithCredentials function.
We execute this function inside handleSubmit and send the email and password.
This function will execute the onClick attribute of the Google button tag.
<buttontype="button"onClick={handleLoginWithGoogle}> Google </button>
Finally in our src/App.tsx file
We are going to use the context by extracting the status and the userID.
We evaluate the status and if it is checking, we show a loading.
const{status,userId}=useContext(AuthContext)if (status==='checking')return<pclassName="loading"><span>Checking credentials, wait a moment...</span></p>
Now at the end of the file we will create two components.
The first one is the HomePage (to simulate that it is a different page).
This component will only be visible when the user is authenticated.
And it will show the userID and a button that executes the close session.
exportconstHomePage=()=>{const{userId,handleLogOut}=useContext(AuthContext)return (<section><h5>Your ID is: <span>{userId}</span></h5><buttonclassName="btn-logout"onClick={handleLogOut}>Log out</button></section>)}
The second component is the AuthPage, (simulates a different page).
This component will only be visible when the user is NOT authenticated.
Only the Login and Register components that we had in our App component are shown.
Now in the App component, we are going to make a validation. Where if the status is authenticated and the userId exists, we show the HomePage, otherwise we show the AuthPage.
import{useContext}from"react"import{Login,Register}from"./components"import{AuthContext}from'./context/authContext';constApp=()=>{const{status,userId}=useContext(AuthContext)if (status==='checking')return<pclassName="loading"><span>Checking credentials, wait a moment...</span></p>return (<main><h1><b>Auth with</b><span>Firebase</span><b>and</b><span>React</span></h1>{(status==='authenticated'&&userId)?<HomePage/>:<AuthPage/>}</main>)}exportdefaultAppexportconstHomePage=()=>{const{userId,handleLogOut}=useContext(AuthContext)return (<section><h5>Your ID is: <span>{userId}</span></h5><buttonclassName="btn-logout"onClick={handleLogOut}>Log out</button></section>)}exportconstAuthPage=()=>{return (<section><Login/><Register/></section>)}
π₯ Conclusion.
No doubt using a service like Firebase helps us a lot in saving time compared to building our own services like authentication.
I hope you liked this post and that it has helped you to understand more about how to perform user authentication in your application with React. π€
If you know any other different or better way to perform this functionality feel free to comment π.
I invite you to check my portfolio in case you are interested in contacting me for a project!. Franklin Martinez Lucas
π΅ Don't forget to follow me also on twitter: @Frankomtz361