Authentication is almost part of every modern web application these days, even the not-so-useful apps still implement some form of authentication because why not? So it doesn't matter whether or not the application requires an authentication system we will add it regardless. That being said, there are lots of different ways to implement authentication in your React application and when I say "implement Authentication" I am talking about consuming an authentication API already set up for you. I've been using different strategies for implementing auth across the different React projects I built especially at the start of my career, however, for the past two years I have realized that there is a reasonable way to manage authentication that doesn't make a pain in the neck. I've stuck to this approach for all my React projects since I stumbled across it.
I am going to ask you, how do you implement the auth flow in your React application? Well if you do whatever you like you might observe that as your application grows and more pages/components are added authentication starts becoming a pain in the neck. However in today's Post, I'm going to show a reasonable way for managing authentication in your React applications, one that scales well with your app.
First of all, I'm going to define three functions, they are just going to be simple functions that mimic the process of creating an account, logging in, logging out, and getting the current user.
// helper.js
export const createAccount = async (email, password) => {
const res = fetch("/create-account", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (res.ok) {
const data = await res.json();
return [null, data];
} else {
const error = await res.json();
return [error, null];
}
};
export const login = async (email, password) => {
const res = fetch("/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (res.ok) {
const data = await res.json();
return [null, data];
} else {
const error = await res.json();
return [error, null];
}
};
export const getUser = async (token) => {
const res = fetch(`/user/${token}`);
if (res.ok) {
const data = await res.json();
return [null, data];
} else {
const error = await res.json();
return [error, null];
}
};
We have defined and exported three functions from the helper file above, now we need to import these functions into our app. So instead of importing these functions directly from this helper file, what we'll do is import all of the functions into our App.jsx
file.
// App.jsx
import { createAccount, login, getUser } from "./helper";
import { createContext, useState, useEffect } from "react";
import router from "./navigation";
import { RouterProvider } from "react-router-dom";
export const AuthContex = createContext({});
function App() {
const [currentUser, setCurrentUser] = useState();
const token = sessionStorage.getItem("userToken");
useEffect(() => {
const runSetup = async () => {
const [error, _user] = await getUser(token);
if (error) {
setCurrentUser(null);
if (
location.pathname !== SCREENS.LOGIN &&
location.pathname !== SCREENS.HOME
) {
location.assign("/login");
}
} else if (_user) {
sessionStorage.setItem("userToken", _user.token);
setCurrentUser(_user);
}
};
}, [token]);
return (
<AuthContext.Provider
value={{
currentUser,
createUser: createAcount,
loginUser: login,
getUser: function () {
return getUser(token);
},
}}
>
<RouterProvider router={router} />
</AuthContext.Provider>
);
}
You might ask yourself well what's the benefit of doing things this way? First, you now have access to all of the authentication functions from inside of any component, you can easily log in as a user from let's say a login page and a login modal. You can also do the same for the createAccount while still maintaining the context of that page. This is especially useful if you just want the user to authenticate themselves and continue with whatever the hell they are doing without having to make them leave the page. This approach has reduced the overall amount of times we import anything from the helper file since they are now available in the global context.
You don't need to worry about retrieving the user token each time you want to retrieve the current User because that has already been done for us, all we need to do is just call the getUser
function. Let's see an example.
import { useContext } from "react";
import { AuthContext } from "../App";
function Component() {
const { user } = useContext(AuthContext);
return (
<div>
<h3>{user.name}</h3>
</div>
);
}
Let's see how logging in a user works,
import { useState, useContext } from "react";
import { AuthContext } from "../App";
function Login() {
const { loginUser } = useContext(AuthContext);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
async function handleSubmit(e) {
e.preventDefault();
const [error, data] = await loginUser(email, password);
if (error) {
alert(error);
console.log(error);
}
if (data) {
alert(data);
console.log(data);
}
}
return (
<form onSubmit={handleSubmit}>
<label>
Email:
<br />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
<label>
Password:
<br />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</label>
<button type="submit">Submit</button>
</form>
);
}
There are lots you could achieve with this pattern, I just wanted to show you this as a starting point you can build on top of. What are your thoughts on this approach? Is this an approach you'd consider? Or you rather use a different approach? Let me know all this and more using the comment section below.