All about JWT (part-3 React-UI)

Javed Akhtar
18 min readFeb 18, 2023

--

The react app we are going to build to understand JWT

Hey! Geeks, welcome to the 3rd-part of my third article on this platform. If you have not gone through part-1 and part-2 of this article then please go through them first from here, I highly recommend it ⚠️.

In the 1st-part of this article I have explained the basics of JWT — “how we are going to implement JWT using Node and React”🔥. In the 2nd-part I have explained — “how you can build your own node server with all the APIs required to understand JWT concept”. In this 3rd-part we will learn how to implement the JWT functionality by building react app and using the Node APIs which we have already built in 2nd-part of this article (which is running locally) 🔥. If you still have doubts, please check my YouTube video 📺 where I have explained this article in detail with code.

Front-end Implementation using React

The aim of the code structure is to explain JWT while keeping the code standard to near to prod which you can further break more to make it more modular. You can check the entire code from my github repository. Clone it and play with it.

Prerequisite❤️‍🔥:

  • Clone the react repo and open it while reading as I have not added all code to this article to keep the article short.
  • Basics understanding of react and how to use APIs in react.

Time saving tips : If you are an experience developer👴🏽 then you might not be interested in basic understanding of code structure. So, you can simply jump🦘 to src/util/MyAxios.js (heart of JWT token handling) section of this article to understand how JWT is implemented with cookies🍪. But if you have time I will still recommend to not skip.

Table of contents

i) Concept summary
ii) We will use following NPM packages📦 in our front-end app
iii) Our front-end code folder structure will look like this
iv) Root directory and files of Source Folder
v) Understanding the source code in detail
a) src/util/MyAxios.js (heart of JWT token handling)
b) src/hooks/useAuth.js (heart of register/login)
c) src/hooks/useHackers.js
d) src/context/AuthContext.js
vi) Understanding files📄 under components folder
a) src/components/Register.js (Register component)
b) src/components/Login.js (Login component)
c) src/components/Home.js (Home component)
d) src/components/Appbar.js (Appbar component)
e) src/components/tophackers/OneTopHacker.js
f) src/components/tophackers/index.js (TopNHackers component)
vii) Important Links
viii) Final Thoughts

Concept summary 🫵

Before diving🤿 into the code, lets try to understand the overall concept which we are going to code.

We are going to create mainly 4 UI pages Registration, Login, Dashboard, Top-Hackers List and a Navigation bar to navigate through pages with logout button. After successful login, front-end will receive 2 JWT tokens, one Access Token and another one Refresh Token. The Access token will be stored in local storage and Refresh token will be stored in Browser cookie🍪 with httpOnly setting(not accessible to front-end script).

Whenever any API will be requested from front-end, the api header will have the Access Token so that the API request will be authenticated✅. If the Access Token🔑 is expired❌, then front-end will raise request for fresh access token with the help of Refresh token (stored in Browser Cookie) and store the fresh Access Token to local-storage for further use. If refresh token is expired then logout the user.

We will use following NPM packages📦 in our front-end app:

Please ignore some dependencies present in package.json file that I have not mentioned here as they are either not in use or not relevant to this tutorial (example testing library).

  1. Material UI : UI framework to develop app faster without thinking on CSS much as this tutorial is focused only on JWT concept and not UI.
  2. axios : to interact with APIs with it’s awesome inbuilt features (like interceptor).
  3. react: The ultimate and most loved UI library for front-end.
  4. react-router-dom : for routing or navigation in the app

Our front-end code folder structure will look like this:

front-end code folder structure

Lets try to understand each one of them:

1. Root directory and files of Source Folder :

a) src/index.js : entry point of the react app, please google it for more.
b) src/index.css, reportWebVitals.js, setupTests.js, App.test.js, App.css :
basic css, react and test files. You can ignore these files as they are not relevant to understand App/JWT implementation.
c) package.json: the package dependency list.
d) package-lock.json: for locking the dependency version installed
e) README.md : The readme file containing info about the app
f) .gitignore: to ignore some files(like node modules) while pushing the code.
g) src/ErrorBoundary.js : Here I have defined a class component and not functional because ErrorBoundary is present only for class component and not for functional component. A class component becomes an error boundary if it defines either (or both) of the lifecycle methods static getDerivedStateFromError() or componentDidCatch(). Use static getDerivedStateFromError() to render a fallback UI after an error has been thrown. Use componentDidCatch() to log error information. For more check this doc.
h) src/ErrorFallback.js : The UI component utilized by ErrorBoundary component to show error❌ page when something breaks in the UI.
i) src/NotFound.js : A component which we will render when there is unrecognized route found in the url of front-end app.
j) src/Privateroute.js : A component which will render child 🧒 component
(functionality of Outlet) when user is logged in and will redirect to login page when user is not signed in i.e. authState.isLogIn ? <Outlet /> : <Navigate to=”/login” />;. So, if we wrap a component under this PrivateRoutecomponent then the component will be rendered only when user is authenticated, hence component will be protected.

import React from "react";
import { Navigate, Outlet } from "react-router-dom";
import { useAuthContext } from "./context/AuthContext";

const PrivateRoute = (props) => {
const { authState } = useAuthContext();

return authState.isLogIn ? <Outlet /> : <Navigate to="/login" />;
};

export default PrivateRoute;

k) src/AppWrapper.js : A wrapper created so that using this we can maintain app’s global state and use it throughout the app without “prop drilling”.
In this file I have created an AuthWrapper component in which I have imported setAuthState, authState (setter, getter) using useAuthContext defined in AuthContext(we will understand it later). Using the setAuthState I am setting auth context of the app based on value of “hacker_user” inside browser localstorage (which gets set when user does login, we will understand it while understanding login component).

So, this is the component which is setting the context for auth related info so that the auth info will be available to all the child components (hence wrapper component). So, the idea is when someone does login successfully,
isLogIn gets set to true and jwtKey also gets set to it’s value. Now, whenever a user gets logged out, isLogIn: false, jwtKey: undefined.

import { useEffect } from "react"
import CircularProgress from '@mui/material/CircularProgress';
import { useAuthContext } from './context/AuthContext'


const AuthWrapper = ({children}) => {

const {setAuthState, authState} = useAuthContext()

useEffect(() => {
const localHackerUser = localStorage.getItem("hacker_user");
if (localHackerUser!==null && localHackerUser!=='undefined') {
console.log('1st....')
const hackerUser = JSON.parse(localHackerUser);
setAuthState(() => ({
isLogIn: true,
jwtKey: hackerUser,
isNewlyRegistered: false,
appWrapperLoading: false
}));
}
else {
console.log('2nd....')
setAuthState((prevData) => ({
...prevData,
isNewlyRegistered: false,
appWrapperLoading: false
}));

}

}, []);

if(authState.appWrapperLoading) // while authWrapper is getting intialised by above useEffect, don't load the app as auth value may not be present while trying to access it in other child comp
return <CircularProgress />

return <>
{children}
</>
}

export { AuthWrapper}

l) src/App.js : A react component which is imported in index.js (entry point of react). This is the component in which we start〽️building our app. So, this file is the base file for the whole react app. In this component I have defined the all protected/unprotected routes of the app using react-router-dom and PrivateRoute component (explained above). And all those routes are wrapped with AuthProvider ErrorBoundary and AuthWrapper to handle errors and provide global data to the components which will render on any route match. As this is not react tutorial, as prerequisite I am assuming that you will understand the code just by reading it. If you have still any issue while trying to understand then you can check my youtube video where I have explained this article with code details.

const App = () => {
return (
<ErrorBoundary>
<AuthProvider>
<AuthWrapper>
<div className="App">
<HashRouter>
<MyAppbar />
<header className="App-header">
<Routes>
<Route exact path="/tophackers" element={<PrivateRoute />}>
<Route exact path="/tophackers" element={<TopNHackers />} />
</Route>
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
<Route exact path="/" element={<Home />} />
<Route exact path="*" element={<NotFound />} />
</Routes>
</header>
</HashRouter>
</div>
</AuthWrapper>
</AuthProvider>
</ErrorBoundary>
);
};

export default App;

Understanding the source code in detail

Since we have already covered all the files present in the root directory and src directory, we will now understand rest folders present in src directory.

src/util/MyAxios.js (heart 💖 of JWT token handling)

This file is basically created to define axios interceptors (please read it from here if it is new to you). Here we are creating an instance of axios using interceptor so that we can update any api call request or response before the request is sent or before the response is available to it’s caller.

This peace of code is very important to us as it is here where we are adding access token 🔑 to each api request as well as handling the expired token and hence renew the access token using refresh token so that user don’t need to login whenenver access token gets expired. Here we are also handling if the refresh token also gets expired, in this case we are logging out the user to login using their credential.

const myAxios = axios.create({
timeout: 5000, // ms
});
myAxios.interceptors.request.use( // requestor interceptor
(request) => requestHandler(request),
(error) => errorHandler(error)
);

myAxios.interceptors.response.use( // response interceptor
(response) => responseHandler(response),
(error) => errorHandler(error)
);

export { myAxios };

Here myAxios is the our version of axios instance which is created with 5000 ms timeout (meaning: it will show timeout error if api does not give response within 500 ms, update this value as per your requirement). We will use myAxios instance to call any api.

With this instance I am also adding request and response interceptor to it using three(as can be seen in above code) functions requestHandler, responseHandler and errorHandler. Let’s try to understand each of them.

i ) requestHandler

const requestHandler = (request) => {
const localHackerUser = localStorage.getItem("hacker_user");
if (localHackerUser) {
request.headers.authorization = `Bearer ${JSON.parse(localHackerUser)}`; // To do: Don't add token to the api call which don't need token
}
return request;
};

This function will be executed whenever there will be an API request raised using the instance myAxios. Here I am first checking if access-token 🔑 is present in the local storage with the name ‘hacker_user’ and if present then add authorization header to the request with token as bearer token🐻.

Note: Bearer tokens🐻 enable requests to authenticate using an access key, it is a way of communication for accessing secured API which says “give access to the bearer of this token”. Aprart from bearer type, there are other types of auth like “basic auth” in which encoded username and password are sent in header. You can think of these auth types as way of communicating what data/mechanism is getting used to grant access.

ii ) responseHandler : For our use case we are not doing anything with reponse we get from API.

iii) errorHandler

This function is the one where we are handling expired ❌ tokens.

const errorHandler = (error) => {
if (error.response.status === 401) {
localStorage.removeItem("hacker_user");
console.log("token error", error.response);
if (error.response.data?.failureCode === "TokenExpiredError") {
axios
.get("http://localhost:4000/hackers/rtoken", { withCredentials: true })
.then((res) => {
localStorage.setItem(
"hacker_user",
JSON.stringify(res.data.data.token)
);
window.location.reload();
})
.catch((err) => {
console.log("refresh token expired :", err);
localStorage.removeItem("hacker_user");
window.location.reload();
});
}
}
return Promise.reject(error);
};

The concept is simple, server is set in such a way that it will reponse 401 error ❌ when there is any issue with JWT token 🔑(access or refresh). If we get 401 error from server then it means that the api call is unauthorized, so the first thing we are doing here is removing token from browser localstorage to make sure that when app is trying to access token from local, it won’t get it because there maybe issue with the access token.

If the 401 error is due to the failureCode TokenExpiredError then it means the access token 🔑 was expired and hence we received 401 unauthorized error. So, instead of logging out the user, we are calling /rtoken endpoint to get a fresh access token and store it in browser localstorage to be used by app. You will notice that I am reloading the app usingwindow.location.reload();
this is to make sure that app gets initialised with the new token 🔑 so that next api call can use the lates fresh token and not the expired one.

If you can remember from part-2 of this article that /rtoken endpoint is used to refresh access token using refresh-token stored in browser
cookie 🍪 which gets attached to /rtoken api request by browser. Now, while generating access token, first server validates that whether the refresh token is expired or not. If expired then it will throw error❌. We are capturing that error and clearing access token from browser followed by a reload of the app to make sure the user gets logged out and hence redirected to login page if trying to access protected ui page at path /tophackers.

Understanding { withCredentials: true }
If you have noticed📝 above errorHandler code, you will see that while calling api /rtoken I am sending another param withCredentials with true value. withCredentials property is a boolean value that indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies, authorization headers or TLS client certificates. In our case we are sending request from localshost running at one port to running localhost to another port, hence we need this flag while sending request.

  • we have covered all code present in the file src/util/MyAxios.js. I will skip sharing the whole file together here to save space. Please clone the reopo from this github account to check the whole file if you have not cloned it in your local machine.

src/hooks/useAuth.js (heart 💖 of register/login)

This is the file where we have defined two custom hooks🪝 useRegister and🪝 useLogin in order to make the logic reuseable in different components (earlier we had to use HOC to reuse logic). Though we are not going to reuse these two hook, it makes sense to separate the api call in different file to keep the code cleaner. Lets understand each one of them.

i) useRegister🪝: This custom hook is called when user submits the register form(in Register component) to get registered to the hacker app.

Image of hacker app showing registration form

This hook accepts name, username, password by a trigger function and returns —
data: the response from /register api.
loading: state indicating the api call is in progress.
error: state representing if there is any error from api call.
trigger: a callback function to trigger the api call with form values.

Note: Here I am not using our myAxios axios instance becasue /register
api endpoint is not secured and hence don’t need JWT token.

i) useLogin 🪝 : This custom hook is called when user submits the login form(in Login component) to get logged in to the hacker app.

Image of hacker app showing login form

The explanation of this hook is same as of useRegister, so I won’t repeat that. The only difference here is the flagcwithCredentials: true used while calling /login api. We are using this flag in the request becasue of following :

withCredentialis used to indicate when cookies 🍪 are to be ignored (if withCredentials=false) in the response. The login reponse contains cookie (containing refresh token) which I want to store in the browser, for that withCredentials flag ⚑ is used.

withCredentialindicates whether or not cross-site Access-Control requests should be made using credentials such as cookies, authorization headers or TLS client certificates. In our case we are sending request from localshost running at one port to running localhost to another port, hence we need this flag while sending request.

  • we have covered all code (ignoring basics of react as this article is not for react but for JWT implementation) present in the file src/hooks/useAuth.js. I will skip sharing the whole file together here to save space. Please clone the reopo from here to check the whole file if you have not cloned it in your local machine.

src/hooks/useHackers.js 🪝

This file is also a hook🪝 useHackers defined in it, same as we have understood in case of useRegister and useLogin, so I will skip explaining the whole code as they are basics of react.

The only difference is it is calling a different endpoint /top/${hackersNumber} which is secured(meaning it needs JWT token to access it) and here we have used our myAxious instance which has interceptor attached to it so that when this gets requested, automatically access token will be attached to the request header so that this secured api can be accessible (already explained while explaining myAxiousinstance creation in util directory).

src/context/AuthContext.js

Here we are defining a react context using createContext which we will use using useContext hook 🪝. This context is used in src/AppWrapper to set auth values and make it available for global use which we have understood earlier section “Root directory and files of Source Folder”.
Now, lets try to understand how this context got created.

import { createContext, useContext, useState } from "react";

const AuthContext = createContext();

const useAuthContext = () => useContext(AuthContext); // to avoid importing both usecontext as well as Authcontext, we can simply import just useAuthContext where we need authcontext

const initialAuthState = {
isLogIn: false,
jwtKey: undefined,
isNewlyRegistered: false,
appWrapperLoading: true,
};

const AuthProvider = ({ children }) => {
const [state, setState] = useState(initialAuthState);

return (
<AuthContext.Provider value={{ authState: state, setAuthState: setState }}>
{children}
</AuthContext.Provider>
);
};

export { useAuthContext, AuthProvider };

In above code we are exporting useAuthContext and AuthProvider.

AuthProvider is a functional component which renders children wrapped in it (check App.js). AuthProvider makes values available to all the nested children wrapped in it. Lets understand how 👉

The value which we want to make available to all the nested children under AuthProvider component, needs to be passed as value to the provider of the AuthContext (i.e. AuthContext.Provider).
Here the value which we have passed is an object containing authState(contains some initial states) and setAuthState(a setter function to update states of authState) i.e. value={{ authState: state, setAuthState: setState }} .

authState contains following states which are accessible to all the nested child components:

isLogIn: We are using it as flag to check if the user is loggedIn
jwtKey: the access token which we are using call secure endpoints
isNewlyRegistered: to show some message to newly registered user.
appWrapperLoading: to check if app is still initializing basic states.

We have understood how to create a context and how to pass values to it to make it available to al the nested children wrapped in it.
But how to use the values passed inside any nested children wrapped in it? Lets understand that now:
To use the context values, we need to use hook called useContext.
This hook takes the context as param while calling it and in reponse it provides the values passed to the context prvider.
e.g const {setAuthState, authState} =useContext(AuthContext) This requires two things to be imported in the component where we want to use the values useContext and AuthContext .

But if you check above code I have not exported AuthContext but instead I have exported useAuthContext. This is to avoid importing both usecontext as well as Authcontextin a component where we wan to use the values.
Now, we can simply import just useAuthContext in any component wrapped inside AuthProvider and call it as function without any param and get the values to be used: e.g. const {setAuthState, authState} = useAuthContext().

Understanding files📄 under components folder

If you are a react developer 💻 and have at least basic knowledge of react and web development then all the components built under src/components folder need not any explanation as they are simple. So, instead of adding all components code here, I will simply summarize the components to explain purpose of their existence and small overview of how it is built. To check the whole code, please clone the repo from here because these components are not the main character but they are just supporting character of the hero of this article i.e. JWT. If you still have some doubts then please check this you Tube series which I have created to explain this article by explaining the code in detail.

a) src/components/Register.js (Register component)

The Register component has a form which contains three mandatory fields. When user submits the details by clicking on “REGISTER” button 🔲, we are checking the validation and if valid then trigger the /register api using the hook useRegister🪝 (already explained above). After the registration button click following are the happening:

  • While api call is in progress, the “REGISTER” button will be disabled.
  • If the response from api says error ❌ then set the error state regError with the error and show this error message in the register page in red.
  • If the user is registered successfully ✅ then set the isNewlyRegistered value to true and redirect the user to login page. isNewlyRegistered is set to true so that when user lands on login page, a message ✉️ will be shown to the user satying “You are successfully registered, please login”.
  • In this component if you notice I have repeated similar code for TextField. So, I have not followed re usability of code here (I am so lazy to do it). So, please create a separate component (say MyTexField) and reuse it to avoid writing extra code or you can keep it as it is while trying to understand JWT but make sure you don’t follow this in PROD 😊

b) src/components/Login.js (Login component)

The Login component has a form which contains two mandatory fields.
When user submits the details by clicking on “LOGIN” button 🔲, we are checking the validation and if valid ✅ then trigger the /login api using the hook useLogin 🪝 (already explained above). After the login button click following are the happening:

  • While api call is in progress, the “LOGIN” button 🔲 will be disabled.
  • check the isNewlyRegistered state, if it is true then show the registration success page to the user.
  • If the response from api says error ❌ then set the error state loginError with the error and show this error message in the login page in red.
  • If the response from API says success ✅, then set the auth (isLogIn jwtKey) variables so that the whole app will be able to use its value as required. We are also setting the JWT token to localstorage so that it can be made available to the app when app reloads itself and lost the state of it.
  • After successfully logged in, redirect the user to home page “/” route.
  • In this component if you notice I have repeated similar code for TextField. So, I have not followed reusibility of code here (I am so lazy to do it). So, please create a separate component (say MyTexField) and reuse it to avoid writing extra code or keep it as it is while trying to understand JWT but make sure you don’t follow this in PROD 😊.

c) src/components/Home.js (Home component)

The Home🏠 component is nothing but just list of images 🖼️.

d) src/components/Appbar.js (Appbar component)

The Appbar component is mainly to show the bar you get to see where we have given page names to navigate to the pages and also a profile circle which also includes the same pages link in a dropdown but it has logout button. The profile circle is only visible when user is logged in.

  • Here the page “Top Hackers” is protected page, meaning it will be visible 👁️ only when user is logged in else user will be redirected to login page (this is done by implementing Privateroute which is already explained above).
  • Some of the elements in the appbar is visible only when the display is wide and some are visible only in mobile screen. This is implemented using display: { xs: “flex”, md: “none” } or display: { xs: “none”, md: “flex” }.
  • Logout functionality : Here when user clicks on menu button🔲 “Logout” from profile icon, I am setting the flag isLogin to false and removing the JWT token from the localstorage and redirecting user to login page. This is how logout is implemented.

e) src/components/tophackers/OneTopHacker.js

Here the component OneTopHacker is a template component which can be used to show details of a hacker based on the details passed to it in props.
This is to make code reusable.

f) src/components/tophackers/index.js (TopNHackers component)

This component is responsible to display all the top-N-Hackers (currently 4) by using the /top/${hackersNumber} api using the hook useHackers (already explained above).

  • Here we are showing circular progress animation while the api fetches the details and once received, we show each hackers details using OneTopHacker component.

Important Links 🔗

Article explained on YouTube

Final Thoughts 💭:

  • I hope this article helped you to understand JWT implementation in front-end(React) and encouraging you to implement it by yourself.
  • I would love to hear your feedback on this article, you can encourage me by clapping👏 and share feedback by commenting what you liked 👍 and what you didn’t 🥲. Please let me know by commenting 💬 if I have explained something which needs to be corrected because at the end, this is how we improve information on internet.
  • It’s 💯 OK if you don’t want to share this article to anyone, but please share your learning to people/developers/juniors/seniors who don’t know yet about JWT implementation, this will serve the sole purpose of this article. Thanks a lot for spending time in reading this article specially in this era of short videos where we get bored so easily.

--

--

Javed Akhtar

I am a software engineer. I love javascript and it's library(React). I am also a writer, cook, singer, youtuber. website: https://javedinfinite.com