Building a System for User Registration and Login using TypeScript (Part 2 )
Learning by Building
Check out Part 1 for the Application Demonstration and building the APIs using Django.
Prerequisite technologies and concepts:
React
TypeScript
React Router
Axios
JSON Web Tokens (JWT)
Hooks:
useState
,useEffect
,createContext
,useContext
Source Code
Developing the Frontend
To begin with, we will be building the front end using React with TypeScript. As a prerequisite, you should have a basic understanding of JavaScript and TypeScript. If you are new to TypeScript, it is recommended to go through some introductory materials to get familiar with the language. Once you grasp the fundamentals of TypeScript, you can proceed to set up a React project.
Check out the React documentation on how to get started with React
Check out the TypeScript documentation on how to get started with TypeScript
You can begin the project by executing this command
npm create vite@latest
To name your project, choose "React" followed by the " TypeScript" option
Navigate to the React app directory
cd VerifyMe_Frontend
Next, install the necessary npm packages
npm install
Note: SCSS was used for styling in this project, however, you are free to use any other styling method you prefer
npm install axios jwt-decode react-icons react-loading react-router-dom sass
Subsequently, initiate the server by executing:
npm run dev
Go to the src
directory and create a pages
directory. Inside the pages
directory, generate HomePage.tsx
, LoginPage.tsx
, and SignupPage.tsx
# /src
cd src
mkdir pages
cd pages
# For Linux:
touch HomePage.tsx LoginPage.tsx SignupPage.tsx
# For Windows:
new-item HomePage.tsx LoginPage.tsx SignupPage.tsx
Note: The main emphasis of this article is not on the layout and appearance of the website. However, you may review the GitHub repository for this project, or come up with your own design
To retrieve the user input on the LoginPage
and transmit the request to the backend server when the submit button is clicked, we'll utilize the useState
hook and establish a handle login function.
// src/pages/LoginPage.tsx
import React, { useState } from "react";
import axios from "axios";
const LoginPage: React.FC = () => {
const [username, setUsername] = useState<string>("");
const [password, setPassword] = useState<string>("");
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
axios.post("http://127.0.0.1:8000/api/token/", {
username, password })
.then((response) => {
console.log(response.data)
})
.catch((error) => {
console.log(error.message);
}
)
}
return (
<div>
<input type="text" value={username} placeholder="Username" onChange={(e) => setUsername(e.target.value)} />
<input type="password" value={password} placeholder="Password" onChange={(e) => setPassword(e.target.value)} />
<button type="submit" onClick={handleLogin}> Submit</button>
</div>
)
};
export default LoginPage;
The SignupPage
will be constructed similarly to the LoginPage
, and we will also create a handleSignup
function
// src/pages/SignupPage.tsx
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
const SignupPage: React.FC = () => {
const [username, setUsername] = useState<string>("");
const [first_name, setFirst_name] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
let navigate = useNavigate()
const handleSignup = (e: React.FormEvent) => {
e.preventDefault();
axios.post("http://127.0.0.1:8000/register/", {
username,
first_name,
email,
password,
})
.then((response) => {
console.log(response.data);
navigate("/login")
})
.catch((error) => {
console.log(error.message);
})
}
return (
<div>
<input type="email" value={email} placeholder="Email" onChange={(e) => setEmail(e.target.value)} />
<input type="text" value={username}
placeholder="Username" onChange={(e) => setUsername(e.target.value)} />
<input type="text" value={first_name} placeholder="Name" onChange={(e) => setFirst_name(e.target.value)} />
<input type="password" value={password} placeholder="Password" onChange={(e) => setPassword(e.target.value)} />
<button type="submit" onClick={handleSignup}> Submit </button>
</div>
)
}
export default SignupPage;
In the Main.tsx
file use Browser router to enable react-router.
// main.tsx or index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Within App.tsx
, configure the routes for both the LoginPage
and SignupPage
// App.tsx
import React from "react";
import { Routes, Route } from "react-router-dom";
import LoginPage from "./pages/LoginPage";
import SignupPage from "./pages/SignupPage";
function App() {
return (
<div className="App">
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignupPage />} />
</Routes>
</div>
)
}
export default App;
To allow requests from the frontend
, enable CORS for localhost:5173
In the backend
settings.py
add CORS ALLOWED WHITELIST
MIDDLEWARE = [
---,
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
---,
]
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
CORS_ALLOWED_WHITELIST = [
"http://localhost:5173",
]
Note: Please keep in mind that if you are running on port 3000
, substitute port
number 5173
with port 3000
Begin the backend server
by executing:
python manage.py runserver
Initiate the frontend server
by executing:
npm run dev
We can assess whether we are capable of registering, logging in, and receiving tokens by testing the system.
Testing Signup.......
Testing Sign-in......
Let's employ the CreateContext
hook and useContext
hook, which assists in distributing data between components
that cannot be easily shared using props
Create a folder named context
in the src
directory. Inside the context
folder, generate a file named AuthContext.tsx
// src/context/AuthContext.tsx
import React, { createContext, useState, ReactNode, useEffect } from "react";
import jwt_decode from "jwt-decode";
import axios from "axios";
Subsequently, let's create a type for the token
received after a successful login
type AccessTokensType = {
access: string | undefined;
refresh: string | undefined;
};
We'll construct an interface for the use states utilized in this file.
interface CurrentUserContextType {
authTokens: AccessTokensType;
setAuthTokens: React.Dispatch<React.SetStateAction<AccessTokensType>>;
user: string | undefined;
setUser: React.Dispatch<React.SetStateAction<string | undefined>>;
loading: boolean;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
callLogout: () => void;
}
All the useState
variables will be mandatory
interface Props {
children: ReactNode;
}
export const AuthContext = createContext<CurrentUserContextType>(
{} as CurrentUserContextType
);
const AuthProvider: React.FC<Props> = ({ children }) => {
let [authTokens, setAuthTokens] = useState<AccessTokensType>(() =>
localStorage.getItem("authTokens")
? JSON.parse(localStorage.getItem("authTokens") || "")
: undefined
);
let [user, setUser] = useState<string | undefined>(() =>
localStorage.getItem("authTokens")
? jwt_decode(localStorage.getItem("authTokens") || "")
: undefined
);
let [loading, setLoading] = useState<boolean>(false);
}
export default AuthProvider;
The authTokens
useStated
is utilized to retrieve and set the value of authTokens
from the browser's localStorage
. We use the method localStorage.getItem("authTokens")
to retrieve the value of the authTokens
key from localStorage
.
The user
useState
is utilized for secure routes, where the user is redirected to the LoginPage
if the variable is undefined
.
The loading
useState
is a boolean state, with false
as its default value
. This is done so that until the tokens are updated, the website does not transmit requests
using expired tokens
, resulting in an error.
Next, we'll generate the function for updating tokens.
// Updating tokens
function updateAccess() {
if (authTokens) {
axios.post("http://127.0.0.1:8000/api/token/refresh/", {
refresh: authTokens.refresh,
})
.then(function (response) {
setAuthTokens(response.data);
localStorage.setItem("authTokens", JSON.stringify(response.data));
setUser(jwt_decode(response.data.access));
setLoading(true);
})
.catch(function (error) {
console.log(error);
});
}
}
Subsequently, we'll produce a function for logging out the user
// calling Log out function
function callLogout() {
setAuthTokens({ access: undefined, refresh: undefined });
setUser(undefined);
localStorage.removeItem("authTokens");
}
After revisiting and accessing the token expiration time, we'll update the refresh token.
// updating the refresh token after revisiting and accessing the token expiration time
useEffect(() => {
if (!loading) {
updateAccess();
}
if (!authTokens) {
setLoading(true);
}
let twentyMinutes = 1000 * 60 * 20;
let interval = setInterval(() => {
if (authTokens) {
updateAccess();
}
}, twentyMinutes);
return () => clearInterval(interval);
}, [authTokens, loading]);
Finally, we'll return the values we want to offer to the children.
return (
<AuthContext.Provider
value={{
setAuthTokens,
authTokens,
setLoading,
loading,
callLogout,
user,
setUser
}}
>
{loading ? children : null}
</AuthContext.Provider>
);
This is what our AuthContext.tsx
file will look like this now
We'll now encase the components in App.tsx
with AuthContext
.
// App.tsx
import AuthProvider from "./context/AuthContext";
function App() {
return (
<AuthProvider>
<div className="App">
------
</div>
</AuthProvider>
)
}
Inside LoginPage.tsx
, we'll import the accessToken
useState
and set the accessToken
to the tokens received after logging in.
In the handleLogin
function, we'll save the response to authTokens
and navigate
to the home page where we'll retrieve the user Info using the token
to get the user data.
// LoginPage.tsx
import React, { useContext } from "react";
import { useNavigate } from "react-router-dom";
import jwt_decode from "jwt-decode";
import { AuthContext } from "../context/AuthContext";sssssssssssssssssssssssssssssssssssssssssss
const { setAuthTokens, setLoading, setUser } = useContext(AuthContext);
let navigate = useNavigate();
const handleLogin = (e: React.FormEvent) => {
----
.then((response) => {
console.log(response.data)
setAuthTokens(response.data);
localStorage.setItem("authTokens", JSON.stringify(response.data));setUser(jwt_decode(response.data.access));
setLoading(true);
navigate("/");
})
----
)
}
Navigate to the HomePage where we going to make a GET request and get the user data by sending the access token.
// HomePage.tsx
import React, {useState, useContext, useEffect} from "react";
import { AuthContext } from "../context/AuthContext";
import axios from "axios";
interface userinfoInterface {
id: number;
first_name: string;
username: string;
email: string;
}
const HomePage: React.FC = () => {
const { authTokens, setLoading } = useContext(AuthContext);
useEffect(() => {
axios.get<userinfoInterface>("http://127.0.0.1:8000/user/", {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + String(authTokens.access),
},
})
.then((response) => {
setUserInfos(response.data);
setLoading(true);
})
.catch((error) => {
console.log(error);
});
}, []);
const [userInfos, setUserInfos] = useState<userinfoInterface>();
return (
<div>
<p>Name: <span>{userInfos?.first_name}</span></p>
<p>Email: <span>{userInfos?.email}</span></p>
<p>Username: <span>{userInfos?.username}</span></p>
</div>
);
};
export default HomePage;
Next, we will create protective routes to restrict access to certain pages for users who have not logged in
Create a folder named utils
inside the src
directory and add a new file named RequireAuth.tsx
.
// src/utils/RequireAuth.tsx
import React, { useContext } from "react";
import { Navigate, Outlet } from "react-router-dom";
import { AuthContext } from "../context/AuthContext";
const RequireAuth: React.FC = () => {
let { user } = useContext(AuthContext);
if (!user) {
return <Navigate to="/login" />;
}
return <Outlet />;
};
export default RequireAuth;
Now, let's add the HomePage
component to the protected routes in App.tsx
import RequireAuth from "./utils/RequireAuth";
import HomePage from "./pages/HomePage";
function App() {
return (
---
<Routes>
<Route element={<RequireAuth />}>
<Route path="/" element={<HomePage />} />
</Route>
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignupPage />} />
</Routes>
---
);
}
Next, let's add a logout button to HomePage.tsx
so that users can log out.
import the callLogout
function
const { callLogout } = useContext(AuthContext);
Create a button to trigger the callLogout
function
<div>
---
<p>Username: <span>{userInfos?.username}</span></p>
<button onClick={callLogout}>Log out</button>
</div>
Summary
This article is a comprehensive tutorial on building a front end for user authentication and authorization using TypeScript and React. It guides the reader through the entire process of setting up the project, creating Login and SignUp pages, creating context, installing packages, utilizing API endpoints for user registration, authentication, and data retrieval, and implementing protective routes for authenticated users. The tutorial is a step-by-step guide with clear instructions, making it easy for readers to follow along and build their own authentication system.
Thank you for reading this article! We hope it was informative and helped you in understanding the process of building a front-end for user sign-up, login, and authentication using TypeScript and React. If you have any questions or feedback, feel free to reach out. Happy coding!