Handle JWT Token expiration in React with Hooks

In previous post, we’ve used JWT for token based authentication (register, login, logout). This tutorial continues to show you how to handle JWT Token expiration in React with Hooks.

Related Posts:
In-depth Introduction to JWT-JSON Web Token
React Refresh Token with JWT and Axios Interceptors
React Custom Hook
React Hooks: JWT Authentication (without Redux) example
React Hooks + Redux: JWT Authentication example

Using React Components instead:
React – How to Logout when Token is expired (JWT)


How to check when JWT Token is expired

There are two ways to check if Token is expired or not.

  • 1. get expiry time in JWT and compare with current time
  • 2. read response status from the server

I will show you the implementations of both ways.
– For 1, we check the token expiration every time the Route changes and call App component logout method.
– For 2, we dispatch logout event to App component when response status tells us the token is expired.

We’re gonna use the code base for next steps. So you need to read one of following tutorials first:
React Hooks: JWT Authentication (without Redux) example
React Hooks + Redux: JWT Authentication example

The Github source code is at the end of the tutorials.

Handle JWT Token expiration with Route changes

We need to do 2 steps:
– Create a component with react-router subscribed to check JWT Token expiry.
– Render it in the App component.

react-router-dom v5

In src folder, create common/AuthVerify.js file with following code:

import React from "react";
import { withRouter } from "react-router-dom";

const parseJwt = (token) => {
  try {
    return JSON.parse(atob(token.split(".")[1]));
  } catch (e) {
    return null;
  }
};

const AuthVerify = (props) => {
  props.history.listen(() => {
    const user = JSON.parse(localStorage.getItem("user"));

    if (user) {
      const decodedJwt = parseJwt(user.accessToken);

      if (decodedJwt.exp * 1000 < Date.now()) {
        props.logOut();
      }
    }
  });

  return <div></div>;
};

export default withRouter(AuthVerify);

Because we use BrowserRouter, we import withRouter and wrap the component with a HoC. Now props can access the history object’s properties and functions. Then we pass a callback to props.history.listen() for listening every Route changes.

To call a parent App component logOut() method from AuthVerify component, we need to pass the logOut() method as a prop:

<AuthVerify logOut={logOut}/>

Let’s put the AuthVerify component into App component like this.

import React, { useState, useEffect, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Router, Switch, Route, Link } from "react-router-dom";

import { logout } from "./actions/auth";

import { history } from "./helpers/history";

import AuthVerify from "./common/AuthVerify";

const App = () => {
  const [showModeratorBoard, setShowModeratorBoard] = useState(false);
  const [showAdminBoard, setShowAdminBoard] = useState(false);

  const { user: currentUser } = useSelector((state) => state.auth);
  const dispatch = useDispatch();

  ..

  useEffect(() => {
    if (currentUser) {
      setShowModeratorBoard(currentUser.roles.includes("ROLE_MODERATOR"));
      setShowAdminBoard(currentUser.roles.includes("ROLE_ADMIN"));
    } else {
      setShowModeratorBoard(false);
      setShowAdminBoard(false);
    }
  }, [currentUser]);
  
  const logOut = () => {
    dispatch(logout());
  };

  return (
    <Router history={history}>
      <div>
        <nav className="navbar navbar-expand navbar-dark bg-dark">
          ...
        </nav>

        <div className="container">
          <Switch>
            <Route exact path={["/", "/home"]} component={Home} />
            <Route exact path="/login" component={Login} />
            <Route exact path="/register" component={Register} />
            <Route exact path="/profile" component={Profile} />
            <Route path="/user" component={BoardUser} />
            <Route path="/mod" component={BoardModerator} />
            <Route path="/admin" component={BoardAdmin} />
          </Switch>
        </div>

        <AuthVerify logOut={logOut}/>
      </div>
    </Router>
  );
};

export default App;

react-router-dom v6

In src folder, create common/AuthVerify.js file with following code:

import React, { useEffect } from "react";
import { useLocation } from "react-router-dom";

const parseJwt = (token) => {
  try {
    return JSON.parse(atob(token.split(".")[1]));
  } catch (e) {
    return null;
  }
};

const AuthVerify = (props) => {
  let location = useLocation();

  useEffect(() => {
    const user = JSON.parse(localStorage.getItem("user"));

    if (user) {
      const decodedJwt = parseJwt(user.accessToken);

      if (decodedJwt.exp * 1000 < Date.now()) {
        props.logOut();
      }
    }
  }, [location, props]);

  return 
; }; export default AuthVerify;

We use useLocation Hook for listening every Route changes.

To call a parent App component logOut() method from AuthVerify component, we need to pass the logOut() method as a prop:

import React, { useState, useEffect, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Routes, Route, Link } from "react-router-dom";

import { logout } from "./actions/auth";

import AuthVerify from "./common/AuthVerify";

const App = () => {
  const [showModeratorBoard, setShowModeratorBoard] = useState(false);
  const [showAdminBoard, setShowAdminBoard] = useState(false);

  const { user: currentUser } = useSelector((state) => state.auth);
  const dispatch = useDispatch();

  ...

  const logOut = useCallback(() => {
    dispatch(logout());
  }, [dispatch]);

  useEffect(() => {
    if (currentUser) {
      setShowModeratorBoard(currentUser.roles.includes("ROLE_MODERATOR"));
      setShowAdminBoard(currentUser.roles.includes("ROLE_ADMIN"));
    } else {
      setShowModeratorBoard(false);
      setShowAdminBoard(false);
    }
  }, [currentUser]);

  return (
    <div>
      <nav className="navbar navbar-expand navbar-dark bg-dark">
        ...
      </nav>

      <div className="container mt-3">
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/home" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route path="/register" element={<Register />} />
          <Route path="/profile" element={<Profile />} />
          <Route path="/user" element={<BoardUser />} />
          <Route path="/mod" element={<BoardModerator />} />
          <Route path="/admin" element={<BoardAdmin />} />
        </Routes>
      </div>

      <AuthVerify logOut={logOut}/>
    </div>
  );
};

export default App;

You also need to move BrowserRouter to src/index.js and wrap the App component.

import { BrowserRouter } from "react-router-dom";
// ...

root.render(
  <Provider store={store}> {/* if using Redux */}
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider> {/* if using Redux */}
);

Handle JWT Token expiration with response status

First we need to set up a global event-driven system, or a PubSub system, which allows us to listen and dispatch events from independent components.

An Event Bus implements the PubSub pattern, events will be fired from other components so that they don’t have direct dependencies between each other.

We’re gonna create Event Bus with three methods: on, dispatch, and remove.

common/EventBus.js

const eventBus = {
  on(event, callback) {
    document.addEventListener(event, (e) => callback(e.detail));
  },
  dispatch(event, data) {
    document.dispatchEvent(new CustomEvent(event, { detail: data }));
  },
  remove(event, callback) {
    document.removeEventListener(event, callback);
  },
};

export default eventBus;

on() method attachs an EventListener to the document object. The callback will be called when the event gets fired.
dispatch() method fires an event using the CustomEvent API.
remove() method removes the attached event from the document object.

Next, we dispatch "logout" event in the components when getting Unauthorized response status.

components/board-user.component.js

import React, { useState, useEffect } from "react";

import UserService from "../services/user.service";
import EventBus from "../common/EventBus";

const BoardUser = () => {
  const [content, setContent] = useState("");

  useEffect(() => {
    UserService.getUserBoard().then(
      (response) => { ... },
      (error) => {
        const _content =
          (error.response &&
            error.response.data &&
            error.response.data.message) ||
          error.message ||
          error.toString();

        setContent(_content);

        if (error.response && error.response.status === 401) {
          EventBus.dispatch("logout");
        }
      }
    );
  }, []);

  return ( ... );
};

export default BoardUser;

Finally we need to import EventBus in App component and listen to "logout" event.

react-router-dom v5

src/App.js

import React, { useState, useEffect, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Router, Switch, Route, Link } from "react-router-dom";

import { logout } from "./actions/auth";

import { history } from "./helpers/history";

import EventBus from "./common/EventBus";

const App = () => {
  const [showModeratorBoard, setShowModeratorBoard] = useState(false);
  const [showAdminBoard, setShowAdminBoard] = useState(false);

  const { user: currentUser } = useSelector((state) => state.auth);
  const dispatch = useDispatch();

  ...

  const logOut = useCallback(() => {
    dispatch(logout());
  }, [dispatch]);

  useEffect(() => {
    if (currentUser) {
      setShowModeratorBoard(currentUser.roles.includes("ROLE_MODERATOR"));
      setShowAdminBoard(currentUser.roles.includes("ROLE_ADMIN"));
    } else {
      setShowModeratorBoard(false);
      setShowAdminBoard(false);
    }

    EventBus.on("logout", () => {
      logOut();
    });

    return () => {
      EventBus.remove("logout");
    };
  }, [currentUser, logOut]);

  return (
    <Router history={history}>
      <div>
        <nav className="navbar navbar-expand navbar-dark bg-dark">
          ...
        </nav>

        <div className="container mt-3">
          <Switch>
            <Route exact path={["/", "/home"]} component={Home} />
            <Route exact path="/login" component={Login} />
            <Route exact path="/register" component={Register} />
            <Route exact path="/profile" component={Profile} />
            <Route path="/user" component={BoardUser} />
            <Route path="/mod" component={BoardModerator} />
            <Route path="/admin" component={BoardAdmin} />
          </Switch>
        </div>
      </div>
    </Router>
  );
};

export default App;

react-router-dom v6

src/App.js

import React, { useState, useEffect, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Routes, Route, Link } from "react-router-dom";

import { logout } from "./actions/auth";

import EventBus from "./common/EventBus";

const App = () => {
  const [showModeratorBoard, setShowModeratorBoard] = useState(false);
  const [showAdminBoard, setShowAdminBoard] = useState(false);

  const { user: currentUser } = useSelector((state) => state.auth);
  const dispatch = useDispatch();

  // ...

  const logOut = useCallback(() => {
    dispatch(logout());
  }, [dispatch]);

  useEffect(() => {
    if (currentUser) {
      setShowModeratorBoard(currentUser.roles.includes("ROLE_MODERATOR"));
      setShowAdminBoard(currentUser.roles.includes("ROLE_ADMIN"));
    } else {
      setShowModeratorBoard(false);
      setShowAdminBoard(false);
    }

    EventBus.on("logout", () => {
      logOut();
    });

    return () => {
      EventBus.remove("logout");
    };
  }, [currentUser, logOut]);

  return (
    <div>
      <nav className="navbar navbar-expand navbar-dark bg-dark">
        ...
      </nav>

      <div className="container mt-3">
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/home" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route path="/register" element={<Register />} />
          <Route path="/profile" element={<Profile />} />
          <Route path="/user" element={<BoardUser />} />
          <Route path="/mod" element={<BoardModerator />} />
          <Route path="/admin" element={<BoardAdmin />} />
        </Routes>
      </div>

    </div>
  );
};

export default App;

Conclusion

Today we’ve known two ways to check check jwt token expiry in React and logout user when the Token is expired.

For the code base, you need to read one of following tutorials first:
React Hooks: JWT Authentication (without Redux) example
React Hooks + Redux: JWT Authentication example

Using React Components instead:
React – How to Logout when Token is expired (JWT)

You can continue to build fullstack Authentication and Authorization system with:
React + Spring Boot: JWT Authentication example
React + Node.js Express: JWT Authentication example

Further Reading

Fullstack CRUD example:
React + Spring Boot + MySQL
React + Spring Boot + PostgreSQL
React + Spring Boot + MongoDB
React + Node.js Express + MySQL
React + Node.js Express + PostgreSQL
React + Node.js Express + MongoDB
React + Django

Simplify import statement with:
Absolute Import in React

Source Code

You can find the complete source code on Github:
React (without Redux).
React + Redux.

React Refresh Token with JWT and Axios Interceptors

One thought to “Handle JWT Token expiration in React with Hooks”

Comments are closed to reduce spam. If you have any question, please send me an email.