Using JWT with React Router

In this article, we will explore the seamless integration of JWT authentication with React and React-router. We’ll also learn how to handle public routes, validation-protected routes, and how to leverage the axios library to make API requests with authentication tokens.

Create a React project

Using the command below will create a project for us

npm create vite@latest react-jwt-cn

Then we choose react and javascript as our framework and language. Before starting the project, we need to ensure that all dependencies have been installed, so we need to execute

npm install

After installation, in the root directory of the project, we can run the following command to start our project

npm run dev

We go through these steps to get our React project up and running smoothly

Install React-Router and Axios

Before we continue, let’s make sure we have installed the necessary dependencies for our project. We’ll start by installing react-router v6, which will handle routing in our React application. Additionally, we will install Axios, a library for sending API requests. By following these steps, we will be equipped with the tools needed to achieve seamless routing and perform efficient API communication. Let’s start by installing these dependencies.

npm install react-router-dom axios

Create AuthProvider and AuthContext

in React

The next thing we need to implement is the JWT authentication function. In this section we will create an AuthProvider component and an associated AuthContext. This will help us store and share JWT authentication related data and functions throughout the application

Create authProvider.js under src > provider. Then let’s explore the implementation of AuthProvider and AuthContext

  1. Import necessary modules and dependencies:

    1. Import axios for sending API requests
    2. Import createContext useContext useEffect useMemo and useState from react >
import axios from "axios";
import {
  createContext,
  useContext,
  useEffect,
  useMemo,
  useState,
} from "react";
  1. Use createContext() to create a context for authentication

    1. The empty context created by createContext() is used to share authentication data and functions between components.
const AuthContext = createContext();

2. Create the AuthProvider component

  1. This component is used as the provider of the authentication context
  2. It receives children as prop, representing the child components that will have access to the authentication context.
const AuthProvider = ({ children }) => {
  //Write component content here
};

3. Use useState to define a state named token

  1. token represents the authentication token
  2. If the token data exists, we will get it through localStorage.getItem("token")
const [token, setToken_] = useState(localStorage.getItem("token"));

4. Create the setToken function to update the authentication token data

  1. This function will be used to update the authentication token
  2. It uses the setToken_ function to update the token data and stores the updated data in the local environment through localStorage.setItem()
const setToken = (newToken) => {
  setToken_(newToken);
};

5. Use useEffect() to set the default authentication request header of axios and save the authentication token data locally.

  1. Whenever token is updated, this effect function will be executed
  2. If token exists, it will be set as axios request header and saved to localStorage
  3. If token is null or undefined, it will remove the corresponding axios request header and localStorage data related to local authentication.
useEffect(() => {
  if (token) {
    axios.defaults.headers.common["Authorization"] = "Bearer " + token;
    localStorage.setItem('token',token);
  } else {
    delete axios.defaults.headers.common["Authorization"];
    localStorage.removeItem('token')
  }
}, [token]);

6. Use useMemo to create memorized contexts

  1. This context contains the token and setToken functions
  2. The value of the token will be used as a memoized dependency (if the token does not change, it will not be re-rendered)
const contextValue = useMemo(
  () => ({
    token,
    setToken,
  }),
  [token]
);

7. Inject authentication context into self-component

  1. Use AuthContext.Provider to wrap child components
  2. Pass contextValue as the value of provider
return (
  <AuthContext.Provider value={contextValue}>
    {children}
  </AuthContext.Provider>
);

8. Export the useAuth hook for external use in the authentication context.

  1. useAuth is a custom hook that allows sub-components to easily access authentication information
export const useAuth = () => {
  return useContext(AuthContext);
};

9. Export AuthProvider by default

export default AuthProvider;

Complete code

import axios from "axios";
import {
  createContext,
  useContext,
  useEffect,
  useMemo,
  useState,
} from "react";

const AuthContext = createContext();

const AuthProvider = ({ children }) => {

    const [token, setToken_] = useState(localStorage.getItem("token"));

    const setToken = (newToken) => {
        setToken_(newToken);
    };

    useEffect(() => {
        if (token) {
          axios.defaults.headers.common["Authorization"] = "Bearer " + token;
          localStorage.setItem('token',token);
        } else {
          delete axios.defaults.headers.common["Authorization"];
          localStorage.removeItem('token')
        }
    }, [token]);
    
    const contextValue = useMemo(
        () => ({
          token,
          setToken,
        }),
        [token]
    );

    return (
        <AuthContext.Provider value={contextValue}>
          {children}
        </AuthContext.Provider>
    );

};

export const useAuth = () => {
    return useContext(AuthContext);
};
  
export default AuthProvider;

To summarize, this code uses React’s context API to set the authentication context. It provides the authentication token and setToken function to the child component through the context. It also ensures that the default authorization request headers in axios are updated promptly when the authentication token is updated.

Create a route for JWT authentication

In order to organize routes more efficiently, we will create a src > routes directory. In this directory, we will create an index.jsx file, which will be used as the entry point for defining the routing of the entire application. By building our routes in separate folders, we can maintain a clear and manageable routing structure. Let’s continue creating routes and explore how to integrate JWT authentication into our React application.

Create a protected routing component for authentication routing

To protect our authenticated route and prevent unauthorized access, we will create a component called ProtectedRoute. This component will wrap our authentication route to ensure that only authorized users have access. By implementing this component, we can easily complete the authentication needs and provide a good user experience. We will create the ProtectedRoute.jsx file under src > routes

  1. First we need to import the necessary dependencies from react-router-dom
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../provider/authProvider";

2. Define the ProtectedRoute component and let it wrap all our routes that require authentication

export const ProtectedRoute = () => {
    const { token } = useAuth();
  
    // Determine whether the user has permissions
    if (!token) {
      //If there is no authorization, jump to the login page
      return <Navigate to="/login" />;
    }
  
    // If authorized, render the subcomponent directly
    return <Outlet />;
 };

3. In the ProtectedRoute component, we obtain token information through the custom hook (useAuth) provided by AuthContext

4. Next we check whether the token exists. If the user is not authorized ( token is faslse or null ), we will route to the login page (/login)

5. If the user is authorized, we will use the Outlet component to render the child route. The Outlet component acts as a placeholder, displaying the child components defined in the parent route.

In summary, the ProtectedRoute component acts as a guard for authenticated routes. If the user is not authenticated, they will be redirected to the login page. If the user is authenticated, the child routes defined in the ProtectedRoute component will be rendered using the Outlet component.

The above code allows us to easily secure specific routes and control access based on the user’s authentication status, providing a secure navigation experience in our React application.

Explore routing in depth

Now that we have the ProtectedRoute component and the authentication context, we can move on to defining our route. By distinguishing between public routes, validation-protected routes, and non-authenticated user routes, we can efficiently handle JWT authentication-based navigation and access control. Next we’ll dive into the src > routes > index.jsx file and explore how to integrate JWT authentication into our routing structure

  1. Import necessary dependencies

    1. RouterProvider and createBrowserRouter are used to configure and provide routing functions
    2. useAuth runs the context in which we access authentication
    3. The ProtectedRoute component wraps the verified route
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { useAuth } from "../provider/authProvider";
import { ProtectedRoute } from "./ProtectedRoute";
  1. Define routing components
    1. This function component serves as the entry point for configuring application routing
const Routes = () => {
  const { token } = useAuth();
  //Write routing configuration here
};
  1. Use useAuth hook to access authentication token

    1. Call useAuth hook to obtain the token from the authentication context
const { token } = useAuth();
  1. Define routes for all users (public routes)

    1. The routesForPublic array protects all routing information that is accessible to all users. Each routing information object contains a path and an element
    2. The path attribute specifies the URL path of the route, and the element attribute points to the jsx component/element that needs to be rendered under the route.
const routesForPublic = [
  {
    path: "/service",
    element: <div>Service Page</div>,
  },
  {
    path: "/about-us",
    element: <div>About Us</div>,
  },
];
  1. Define routes that only authorized users can access

    1. The routesForAuthenticatedOnly array contains route objects that can only be accessed by authenticated users. It includes the protected root route (“/”) wrapped in a ProtectedRoute component and additional child routes defined using the children attribute.
const routesForAuthenticatedOnly = [
  {
    path: "/",
    element: <ProtectedRoute />,
    children: [
      {
        path: "/",
        element: <div>User Home Page</div>,
      },
      {
        path: "/profile",
        element: <div>User Profile</div>,
      },
      {
        path: "/logout",
        element: <div>Logout</div>,
      },
    ],
  },
];
  1. Define routes that only unauthorized users can access

    1. The routesForNotAuthenticatedOnly array contains route objects that are not accessible to authenticated users. It contains the login route (/login)
const routesForNotAuthenticatedOnly = [
  {
    path: "/",
    element: <div>Home Page</div>,
  },
  {
    path: "/login",
    element: <div>Login</div>,
  },
];

Compose and determine routes based on authentication status

  1. The createBrowserRouter function is used to create routing configuration. It receives a routing array as an input parameter.
  2. The spread operator (…) is used to combine multiple route arrays into a single array
  3. The conditional expression (!token ? routesForNotAuthenticatedOnly : []) checks whether the user is authenticated (token exists). If not, it contains the routesForNotAuthenticatedOnly array; otherwise, it contains an empty array.
const router = createBrowserRouter([
  ...routesForPublic,
  ...(!token ? routesForNotAuthenticatedOnly : []),
  ...routesForAuthenticatedOnly,
]);
  1. Use RouterProvider to inject routing configuration

    1. The RouterProvider component wraps routing configuration and makes it available to the entire application
return <RouterProvider router={router} />;

Complete code

import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { useAuth } from "../provider/authProvider";
import { ProtectedRoute } from "./ProtectedRoute";

const Routes = () => {
  const { token } = useAuth();

  //Public routing configuration
  const routesForPublic = [
    {
      path: "/service",
      element: 
Service Page
, }, { path: "/about-us", element:
About Us
, }, ]; // Routing configuration that only authorized users can access const routesForAuthenticatedOnly = [ { path: "/", element: , // Wrap the component in ProtectedRoute children: [ { path: "/", element:
User Home Page
, }, { path: "/profile", element:
User Profile
, }, { path: "/logout", element:
Logout
, }, ], }, ]; // Routing configuration that can only be accessed by unauthorized users const routesForNotAuthenticatedOnly = [ { path: "/", element:
Home Page
, }, { path: "/login", element:
Login
, }, ]; // Merge routing configuration const router = createBrowserRouter([ ...routesForPublic, ...(!token ? routesForNotAuthenticatedOnly : []), ...routesForAuthenticatedOnly, ]); return <RouterProvider router={router} />; }; export default Routes;

Final integration

Now we have AuthContext, AuthProvider and Routes ready. Let’s integrate them into App.jsx

  1. Import necessary components and files

    1. AuthProvider is a component imported from the ./provider/authProvider file. It provides the authentication context for the entire application
    2. Import Routes from ./routes. It defines application routing
import AuthProvider from "./provider/authProvider";
import Routes from "./routes";
  1. Use the AuthProvider component to wrap the Routes component

    1. The AuthProvider component is used to provide authentication context to the application. It wraps the Routes component, making the authentication context available to all components in the Routes component tree
return (
  <AuthProvider>
    <Routes />
  </AuthProvider>
);

Complete code

import AuthProvider from "./provider/authProvider";
import Routes from "./routes";

function App() {
  return (
    <AuthProvider>
      <Routes />
    </AuthProvider>
  );
}

export default App;

Realize login and logout

Create login page in src > pages > Login.jsx

const Login = () => {
  const { setToken } = useAuth();
  const navigate = useNavigate();

  const handleLogin = () => {
    setToken("this is a test token");
    navigate("/", { replace: true });
  };

  setTimeout(() => {
    handleLogin();
  }, 3 * 1000);

  return <>Login Page</>;
};

export default Login;
  • The login component is a function component used to represent the login page
  • Use useAuth hook to import the setToken function from the authentication context
  • Import the navigate function from react-router-dom to handle routing jumps
  • Inside the component, there is a handleLogin function that sets the test token using the setToken function in the context and navigates to the homepage (“/”) with the replace option set to true
  • The setTimeout function is used to simulate a 3-second delay before executing the handleLogin function.
  • The component returns JSX for the login page, where it acts as a placeholder text

Now, we create a logout page in src > pages > Logout.jsx

import { useNavigate } from "react-router-dom";
import { useAuth } from "../provider/authProvider";

const Logout = () => {
  const { setToken } = useAuth();
  const navigate = useNavigate();

  const handleLogout = () => {
    setToken();
    navigate("/", { replace: true });
  };

  setTimeout(() => {
    handleLogout();
  }, 3 * 1000);

  return <>Logout Page</>;
};

export default Logout;
  • In the logout page, we called the setToken function without passing parameters, which is equivalent to calling setToken(null)

Now we will replace the login and logout components in the routing component with updated versions

const routesForNotAuthenticatedOnly = [
  {
    path: "/",
    element: <div>Home Page</div>,
  },
  {
    path: "/login",
    element: <Login />,
  },
];

In the routesForNotAuthenticatedOnly array, the element attribute of “/login” is set to , which means that when the user When accessing the "/login" path, the Login component is rendered

const routesForAuthenticatedOnly = [
  {
    path: "/",
    element: <ProtectedRoute />,
    children: [
      {
        path: "/",
        element: <div>User Home Page</div>,
      },
      {
        path: "/profile",
        element: <div>User Profile</div>,
      },
      {
        path: "/logout",
        element: <Logout />,
      },
    ],
  },
];

In the routesForAuthenticatedOnly array, the element attribute of “/logout” is set to , which means that when the user When accessing the "/logout" path, the Logout component is rendered

Testing process

  1. When you first visit the root page /, you will see “Home page” in the routesForNotAuthenticatedOnly array
  2. If you navigate to /login, after a 3 second delay, the login process will be simulated. It will set the test token using the setToken function in the authentication context, and then you will be redirected to the root page / by the navigation functions in the react-router-dom library. After redirection, you will see “User Home Page” in the routesForAuthenticatedOnly array
  3. If you then access /logout, after a 3 second delay, the logout process will be simulated. It will clear the authentication token by calling the setToken function without any parameters and then you will be redirected to the root page / . Since you are now logged out, we will see ” Home Page ” in the routesForNotAuthenticatedOnly array.

This flow demonstrates the login and logout process where the user transitions between authenticated and unauthenticated states and the corresponding routes are displayed accordingly.

The above is the entire content of this article. Thank you everyone for your support of this article ~ Welcome to like and collect it, and leave your opinions in the comment area