You are currently viewing Effortless User Management in React Redux: Login, Logout, and Registration Example with Hooks
Reactjs

Effortless User Management in React Redux: Login, Logout, and Registration Example with Hooks

Mastering User Authentication with React Redux: Login, Logout, and Registration Example using Hooks, LocalStorage, React Router, Axios, and Bootstrap

In this comprehensive tutorial, we will construct a robust user authentication system using React Redux. You will gain hands-on experience in:

  1. Implementing JWT Authentication Flow for User Registration, Login, and Logout
  2. Establishing an Organized Project Structure for React Redux with JWT Authentication, LocalStorage, Router, and Axios Integration
  3. Harnessing the Power of Redux: Actions, Reducers, and Store Management for Seamless Application State
  4. Crafting Dynamic React Function Components using Hooks and Enabling Form Validation
  5. Building React Function Components to Access Protected Resources with Authorization
  6. Creating a Dynamic Navigation Bar to Enhance User Experience within Your React App

By the end of this tutorial, you will have a deep understanding of React Redux, and you’ll be equipped with the skills to create a sophisticated user authentication system in your projects.

Overview of React Redux Registration & Login example

We will build a React.js application using Hooks in that:

  • There are Login/Logout, Signup pages.
  • Form data will be validated by front-end before being sent to back-end.
  • Depending on User’s roles (admin, moderator, user), Navigation Bar changes its items automatically.

Seamless User Registration and Login Process
Implementing JWT Authentication involves utilizing two key endpoints:

  1. POST api/auth/signup: This endpoint facilitates User Registration.
  2. POST api/auth/signin: This endpoint manages User Login.

The outlined flow provides a concise depiction of the Requests and Responses orchestrated by the React.js Client. To access protected resources, the React Client must ingeniously include a JWT token within the HTTP Header, effectively fortifying each request with the necessary authorization.

– The App page acts as a comprehensive container, orchestrated by React Router, adept at harnessing the app state sourced from the Redux Store. Consequently, the dynamically responsive navbar seamlessly adjusts based on this state.

– Both the Login and Register pages, equipped with intelligently designed forms for streamlined data submission (further empowered by the react-validation library), effectively dispatch auth actions (login/register) to the Redux Thunk Middleware. This intermediary then collaborates with the auth.service to seamlessly orchestrate API calls.

– The auth.service methods leverage the robust axios library, serving as the conduit for our HTTP requests. An additional layer of sophistication is introduced, as these methods proficiently manage the storage and retrieval of the JWT token from the Local Storage within the confines of the user’s browser.

– The Home page, a publicly accessible haven, warmly welcomes all visitors.

– Upon successful execution of the login action, the Profile page elegantly showcases pertinent user information, fostering a personalized experience.

– With an air of dynamicity, the BoardUser, BoardModerator, and BoardAdmin pages gracefully manifest within the navbar, contingent upon the user’s roles stored in the state. In this intriguing realm, the user.service takes center stage, providing an avenue to interact with protected resources ensconced within the Web API.

– At the core of the user.service lies the ingenious auth-header() helper function, meticulously engineered to seamlessly integrate the JWT into the HTTP header. This function, in turn, conjures an object, replete with the JWT of the presently logged-in user, plucked judiciously from the confines of the revered Local Storage.

Technology

We’re gonna use these modules:

  • React 18/17
  • react-redux 8.0.1
  • redux 4.2.0
  • redux-thunk 2.4.1
  • react-router-dom 6
  • axios 0.27.2
  • react-validation 3.0.7
  • Bootstrap 4
  • validator 13.7.0

Project Structure

This is folders & files structure for this React Redux Registration and Login application:

image

With the explanation in diagram above, you can understand the project structure easily.

But I need to say some things about Redux elements that we’re gonna use:
– actions folder contains all the action creators (auth.js for register & login, message.js for response message from server).
– reducers folder contains all the reducers, each reducer updates a different part of the application state corresponding to dispatched action.

Setup React.js Project

Open cmd at the folder you want to save Project folder, run command:
npx create-react-app react-redux-hooks-jwt-auth

Then add Router Dom Modulenpm install react-router-dom.

Open src/index.js, import BrowserRouter and wrap the App component:

import React from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";

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

const container = document.getElementById("root");
const root = createRoot(container);

root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

Import Bootstrap

Run command: npm install bootstrap.

Open src/App.js and modify the code inside it as following-

import React from "react";
import "bootstrap/dist/css/bootstrap.min.css";

const App = () => {
  // ...
}

export default App;

Create Services

We’re gonna create two services in src/services folder:

  • Authentication service
  • Data service

 services

 auth-header.js

 auth.service.js (Authentication service)

 user.service.js (Data service)


Before working with these services, we need to install Axios with command:
npm install axios

Authentication service

The service uses Axios for HTTP requests and Local Storage for user information & JWT.
It provides following important functions:

  • register(): POST {username, email, password}
  • login(): POST {username, password} & save JWT to Local Storage
  • logout(): remove JWT from Local Storage

services/auth.service.js

import axios from "axios";

const API_URL = "http://localhost:8080/api/auth/";

const register = (username, email, password) => {
  return axios.post(API_URL + "signup", {
    username,
    email,
    password,
  });
};

const login = (username, password) => {
  return axios
    .post(API_URL + "signin", {
      username,
      password,
    })
    .then((response) => {
      if (response.data.accessToken) {
        localStorage.setItem("user", JSON.stringify(response.data));
      }

      return response.data;
    });
};

const logout = () => {
  localStorage.removeItem("user");
};

export default {
  register,
  login,
  logout,
};

Data service

We also have methods for retrieving data from server. In the case we access protected resources, the HTTP request needs Authorization header.

Let’s create a helper function called authHeader() inside auth-header.js:

export default function authHeader() {
  const user = JSON.parse(localStorage.getItem('user'));

  if (user && user.accessToken) {
    return { Authorization: 'Bearer ' + user.accessToken };
  } else {
    return {};
  }
}

The code above checks Local Storage for user item. If there is a logged in user with accessToken (JWT), return HTTP Authorization header. Otherwise, return an empty object.


Note: For Node.js Express back-end, please use x-access-token header like this:

export default function authHeader() {
  const user = JSON.parse(localStorage.getItem('user'));

  if (user && user.accessToken) {
    // for Node.js Express back-end
    return { 'x-access-token': user.accessToken };
  } else {
    return {};
  }
}

Now we define a service for accessing data in services/user.service.js:

import axios from "axios";
import authHeader from "./auth-header";

const API_URL = "http://localhost:8080/api/test/";

const getPublicContent = () => {
  return axios.get(API_URL + "all");
};

const getUserBoard = () => {
  return axios.get(API_URL + "user", { headers: authHeader() });
};

const getModeratorBoard = () => {
  return axios.get(API_URL + "mod", { headers: authHeader() });
};

const getAdminBoard = () => {
  return axios.get(API_URL + "admin", { headers: authHeader() });
};

export default {
  getPublicContent,
  getUserBoard,
  getModeratorBoard,
  getAdminBoard,
};

You can see that we add a HTTP header with the help of authHeader() function when requesting authorized resource.

Create Redux Actions

We’re gonna create two kind of actions in src/actions folder:


 actions

 types.js

 auth.js (register/login/logout actions)

 message.js (set/clear message actions)


Action Types

First we defined some string constant that indicates the type of action being performed.

actions/type.js

export const REGISTER_SUCCESS = "REGISTER_SUCCESS";
export const REGISTER_FAIL = "REGISTER_FAIL";
export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
export const LOGIN_FAIL = "LOGIN_FAIL";
export const LOGOUT = "LOGOUT";

export const SET_MESSAGE = "SET_MESSAGE";
export const CLEAR_MESSAGE = "CLEAR_MESSAGE";

Message Actions Creator

This Redux action creator is for actions related to messages (notifications) from APIs.

actions/message.js

import { SET_MESSAGE, CLEAR_MESSAGE } from "./types";

export const setMessage = (message) => ({
  type: SET_MESSAGE,
  payload: message,
});

export const clearMessage = () => ({
  type: CLEAR_MESSAGE,
});

Auth Actions Creator

This is creator for actions related to authentication. We’re gonna import AuthService to make asynchronous HTTP requests with trigger one or more dispatch in the result.

– register()

  • calls the AuthService.register(username, email, password)
  • dispatch REGISTER_SUCCESS and SET_MESSAGE if successful
  • dispatch REGISTER_FAIL and SET_MESSAGE if failed

– login()

  • calls the AuthService.login(username, password)
  • dispatch LOGIN_SUCCESS and SET_MESSAGE if successful
  • dispatch LOGIN_FAIL and SET_MESSAGE if failed

Both action creators return a Promise for Components using them.

actions/auth.js

import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
  LOGIN_SUCCESS,
  LOGIN_FAIL,
  LOGOUT,
  SET_MESSAGE,
} from "./types";

import AuthService from "../services/auth.service";

export const register = (username, email, password) => (dispatch) => {
  return AuthService.register(username, email, password).then(
    (response) => {
      dispatch({
        type: REGISTER_SUCCESS,
      });

      dispatch({
        type: SET_MESSAGE,
        payload: response.data.message,
      });

      return Promise.resolve();
    },
    (error) => {
      const message =
        (error.response &&
          error.response.data &&
          error.response.data.message) ||
        error.message ||
        error.toString();

      dispatch({
        type: REGISTER_FAIL,
      });

      dispatch({
        type: SET_MESSAGE,
        payload: message,
      });

      return Promise.reject();
    }
  );
};

export const login = (username, password) => (dispatch) => {
  return AuthService.login(username, password).then(
    (data) => {
      dispatch({
        type: LOGIN_SUCCESS,
        payload: { user: data },
      });

      return Promise.resolve();
    },
    (error) => {
      const message =
        (error.response &&
          error.response.data &&
          error.response.data.message) ||
        error.message ||
        error.toString();

      dispatch({
        type: LOGIN_FAIL,
      });

      dispatch({
        type: SET_MESSAGE,
        payload: message,
      });

      return Promise.reject();
    }
  );
};

export const logout = () => (dispatch) => {
  AuthService.logout();

  dispatch({
    type: LOGOUT,
  });
};

Create Redux Reducers

There will be two reducers in src/reducers folder, each reducer updates a different part of the state corresponding to dispatched Redux actions.


 reducers

 index.js

 auth.js (register/login/logout)

 message.js (set/clear message)


Message Reducer

This reducer updates message state when message action is dispatched from anywhere in the application.

reducers/message.js

import { SET_MESSAGE, CLEAR_MESSAGE } from "../actions/types";

const initialState = {};

export default function (state = initialState, action) {
  const { type, payload } = action;

  switch (type) {
    case SET_MESSAGE:
      return { message: payload };

    case CLEAR_MESSAGE:
      return { message: "" };

    default:
      return state;
  }
}

Auth Reducer

The Auth reducer will update the isLoggedIn and user state of the application.

reducers/auth.js

import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
  LOGIN_SUCCESS,
  LOGIN_FAIL,
  LOGOUT,
} from "../actions/types";

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

const initialState = user
  ? { isLoggedIn: true, user }
  : { isLoggedIn: false, user: null };

export default function (state = initialState, action) {
  const { type, payload } = action;

  switch (type) {
    case REGISTER_SUCCESS:
      return {
        ...state,
        isLoggedIn: false,
      };
    case REGISTER_FAIL:
      return {
        ...state,
        isLoggedIn: false,
      };
    case LOGIN_SUCCESS:
      return {
        ...state,
        isLoggedIn: true,
        user: payload.user,
      };
    case LOGIN_FAIL:
      return {
        ...state,
        isLoggedIn: false,
        user: null,
      };
    case LOGOUT:
      return {
        ...state,
        isLoggedIn: false,
        user: null,
      };
    default:
      return state;
  }
}

Combine Reducers

Because we only have a single store in a Redux application. We use reducer composition instead of many stores to split data handling logic.

reducers/index.js

import { combineReducers } from "redux";
import auth from "./auth";
import message from "./message";

export default combineReducers({
  auth,
  message,
});

Create Redux Store

This Store will bring Actions and Reducers together and hold the Application state.

Now we need to install Redux, Thunk Middleware and Redux Devtool Extension.
Run the command:

npm install redux redux-thunk
npm install --save-dev redux-devtools-extension

In the previous section, we used combineReducers() to combine 2 reducers into one. Let’s import it, and pass it to createStore():

store.js

import { createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import rootReducer from "./reducers";

const middleware = [thunk];

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(...middleware))
);

export default store;

Make Redux Store available

We will wrap our application with a <Provider> component. It makes the Redux store available to any nested components.

src/index.js

import React from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import store from "./store";
import "./index.css";
import App from "./App";

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

const container = document.getElementById("root");
const root = createRoot(container);

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

Create React Pages for Authentication

In src folder, create new folder named components and add several files as following:


 components

 Login.js

 Register.js

 Profile.js


Form Validation overview

Now we need a library for Form validation, so we’re gonna add react-validation library to our project.
Run the command: npm install react-validation validator

To use react-validation in this example, you need to import following items:

import Form from "react-validation/build/form";
import Input from "react-validation/build/input";
import CheckButton from "react-validation/build/button";

import { isEmail } from "validator";

We also use isEmail() function from validator to verify email.

This is how we put them in render() method with validations attribute:

const required = value => {
  if (!value) {
    return (
      <div className="alert alert-danger" role="alert">
        This field is required!
      </div>
    );
  }
};

const email = value => {
  if (!isEmail(value)) {
    return (
      <div className="alert alert-danger" role="alert">
        This is not a valid email.
      </div>
    );
  }
};

render() {
  return (
  ...
    <Form
      onSubmit={handleLogin}
      ref={form}
    >
      ...
      <Input
        type="text"
        className="form-control"
        ...
        validations={[required, email]}
      />

      <CheckButton
        style={{ display: "none" }}
        ref={checkBtn}
      />
    </Form>
  ...
  );
}

We’re gonna call Form validateAll() method to check validation functions in validations. Then CheckButton helps us to verify if the form validation is successful or not. So this button will not display on the form.

form.validateAll();

if (checkBtn.context._errors.length === 0) {
  // do something when no error
}

If you need Form Validation with React Hook Form 7

Login Page

This page has a Form with username & password.
– We’re gonna verify them as required field.
– If the verification is ok, we dispatch login action, then direct user to Profile page: navigate("/profile");, or show message with response error.

For getting the application state and dispatching actions, we use React Redux Hooks useSelector and useDispatch.
– by checking isLoggedIn, we can redirect user to Profile page.
– message gives us response message.

components/Login.js

import React, { useState, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Navigate, useNavigate  } from 'react-router-dom';

import Form from "react-validation/build/form";
import Input from "react-validation/build/input";
import CheckButton from "react-validation/build/button";

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

const required = (value) => {
  if (!value) {
    return (
      <div className="alert alert-danger" role="alert">
        This field is required!
      </div>
    );
  }
};

const Login = (props) => {
  let navigate = useNavigate();

  const form = useRef();
  const checkBtn = useRef();

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);

  const { isLoggedIn } = useSelector(state => state.auth);
  const { message } = useSelector(state => state.message);

  const dispatch = useDispatch();

  const onChangeUsername = (e) => {
    const username = e.target.value;
    setUsername(username);
  };

  const onChangePassword = (e) => {
    const password = e.target.value;
    setPassword(password);
  };

  const handleLogin = (e) => {
    e.preventDefault();

    setLoading(true);

    form.current.validateAll();

    if (checkBtn.current.context._errors.length === 0) {
      dispatch(login(username, password))
        .then(() => {
          navigate("/profile");
          window.location.reload();
        })
        .catch(() => {
          setLoading(false);
        });
    } else {
      setLoading(false);
    }
  };

  if (isLoggedIn) {
    return <Navigate to="/profile" />;
  }

  return (
    <div className="col-md-12">
      <div className="card card-container">
        <img
          src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
          alt="profile-img"
          className="profile-img-card"
        />

        <Form onSubmit={handleLogin} ref={form}>
          <div className="form-group">
            <label htmlFor="username">Username</label>
            <Input
              type="text"
              className="form-control"
              name="username"
              value={username}
              onChange={onChangeUsername}
              validations={[required]}
            />
          </div>

          <div className="form-group">
            <label htmlFor="password">Password</label>
            <Input
              type="password"
              className="form-control"
              name="password"
              value={password}
              onChange={onChangePassword}
              validations={[required]}
            />
          </div>

          <div className="form-group">
            <button className="btn btn-primary btn-block" disabled={loading}>
              {loading && (
                <span className="spinner-border spinner-border-sm"></span>
              )}
              <span>Login</span>
            </button>
          </div>

          {message && (
            <div className="form-group">
              <div className="alert alert-danger" role="alert">
                {message}
              </div>
            </div>
          )}
          <CheckButton style={{ display: "none" }} ref={checkBtn} />
        </Form>
      </div>
    </div>
  );
};

export default Login;

Register Page

This page is similar to Login Page.

For Form Validation, there are some more details:

  • username: required, between 3 and 20 characters
  • email: required, email format
  • password: required, between 6 and 40 characters

We’re gonna dispatch register action and show response message (successful or error).

components/Register.js

import React, { useState, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";

import Form from "react-validation/build/form";
import Input from "react-validation/build/input";
import CheckButton from "react-validation/build/button";
import { isEmail } from "validator";

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

const required = (value) => {
  if (!value) {
    return (
      <div className="alert alert-danger" role="alert">
        This field is required!
      </div>
    );
  }
};

const validEmail = (value) => {
  if (!isEmail(value)) {
    return (
      <div className="alert alert-danger" role="alert">
        This is not a valid email.
      </div>
    );
  }
};

const vusername = (value) => {
  if (value.length < 3 || value.length > 20) {
    return (
      <div className="alert alert-danger" role="alert">
        The username must be between 3 and 20 characters.
      </div>
    );
  }
};

const vpassword = (value) => {
  if (value.length < 6 || value.length > 40) {
    return (
      <div className="alert alert-danger" role="alert">
        The password must be between 6 and 40 characters.
      </div>
    );
  }
};

const Register = () => {
  const form = useRef();
  const checkBtn = useRef();

  const [username, setUsername] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [successful, setSuccessful] = useState(false);

  const { message } = useSelector(state => state.message);
  const dispatch = useDispatch();

  const onChangeUsername = (e) => {
    const username = e.target.value;
    setUsername(username);
  };

  const onChangeEmail = (e) => {
    const email = e.target.value;
    setEmail(email);
  };

  const onChangePassword = (e) => {
    const password = e.target.value;
    setPassword(password);
  };

  const handleRegister = (e) => {
    e.preventDefault();

    setSuccessful(false);

    form.current.validateAll();

    if (checkBtn.current.context._errors.length === 0) {
      dispatch(register(username, email, password))
        .then(() => {
          setSuccessful(true);
        })
        .catch(() => {
          setSuccessful(false);
        });
    }
  };

  return (
    <div className="col-md-12">
      <div className="card card-container">
        <img
          src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
          alt="profile-img"
          className="profile-img-card"
        />

        <Form onSubmit={handleRegister} ref={form}>
          {!successful && (
            <div>
              <div className="form-group">
                <label htmlFor="username">Username</label>
                <Input
                  type="text"
                  className="form-control"
                  name="username"
                  value={username}
                  onChange={onChangeUsername}
                  validations={[required, vusername]}
                />
              </div>

              <div className="form-group">
                <label htmlFor="email">Email</label>
                <Input
                  type="text"
                  className="form-control"
                  name="email"
                  value={email}
                  onChange={onChangeEmail}
                  validations={[required, validEmail]}
                />
              </div>

              <div className="form-group">
                <label htmlFor="password">Password</label>
                <Input
                  type="password"
                  className="form-control"
                  name="password"
                  value={password}
                  onChange={onChangePassword}
                  validations={[required, vpassword]}
                />
              </div>

              <div className="form-group">
                <button className="btn btn-primary btn-block">Sign Up</button>
              </div>
            </div>
          )}

          {message && (
            <div className="form-group">
              <div className={ successful ? "alert alert-success" : "alert alert-danger" } role="alert">
                {message}
              </div>
            </div>
          )}
          <CheckButton style={{ display: "none" }} ref={checkBtn} />
        </Form>
      </div>
    </div>
  );
};

export default Register;

Profile Page

This page gets current User from Local Storage by getting user in the application state and show user information (with token).
If the user is not logged in, navigate to /login page.

components/Profile.js

import React from "react";
import { Navigate } from 'react-router-dom';
import { useSelector } from "react-redux";

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

  if (!currentUser) {
    return <Navigate to="/login" />;
  }

  return (
    <div className="container">
      <header className="jumbotron">
        <h3>
          <strong>{currentUser.username}</strong> Profile
        </h3>
      </header>
      <p>
        <strong>Token:</strong> {currentUser.accessToken.substring(0, 20)} ...{" "}
        {currentUser.accessToken.substr(currentUser.accessToken.length - 20)}
      </p>
      <p>
        <strong>Id:</strong> {currentUser.id}
      </p>
      <p>
        <strong>Email:</strong> {currentUser.email}
      </p>
      <strong>Authorities:</strong>
      <ul>
        {currentUser.roles &&
          currentUser.roles.map((role, index) => <li key={index}>{role}</li>)}
      </ul>
    </div>
  );
};

export default Profile;

Create React Pages for accessing Resources

These pages will use UserService to request data from API.


 components

 Home.js

 BoardUser.js

 BoardModerator.js

 BoardAdmin.js


Home Page

This is a public page that shows public content. People don’t need to log in to view this page.

components/Home.js

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

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

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

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

        setContent(_content);
      }
    );
  }, []);

  return (
    <div className="container">
      <header className="jumbotron">
        <h3>{content}</h3>
      </header>
    </div>
  );
};

export default Home;

Role-based Pages

We’re gonna have 3 pages for accessing protected data:

  • BoardUser page calls UserService.getUserBoard()
  • BoardModerator page calls UserService.getModeratorBoard()
  • BoardAdmin page calls UserService.getAdminBoard()

I will show you User Page for example, other Pages are similar to this Page.

components/BoardUser.js

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

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

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

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

        setContent(_content);
      }
    );
  }, []);

  return (
    <div className="container">
      <header className="jumbotron">
        <h3>{content}</h3>
      </header>
    </div>
  );
};

export default BoardUser;

Add Navbar and define Routes

Add React Router

Modify App Page

Now we add a navigation bar in App Page. This is the root container for our application.
The navbar dynamically changes by login status and current User’s roles.

  • Home: always
  • Login & Sign Up: if user hasn’t signed in yet
  • User: there is user value in the application state
  • Board Moderator: roles includes ROLE_MODERATOR
  • Board Admin: roles includes ROLE_ADMIN

src/App.js

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

import "bootstrap/dist/css/bootstrap.min.css";
import "./App.css";

import Login from "./components/Login";
import Register from "./components/Register";
import Home from "./components/Home";
import Profile from "./components/Profile";
import BoardUser from "./components/BoardUser";
import BoardModerator from "./components/BoardModerator";
import BoardAdmin from "./components/BoardAdmin";

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

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

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

  let location = useLocation();

  useEffect(() => {
    if (["/login", "/register"].includes(location.pathname)) {
      dispatch(clearMessage()); // clear message when changing location
    }
  }, [dispatch, location]);

  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">
        <Link to={"/"} className="navbar-brand">
          codeacademia
        </Link>
        <div className="navbar-nav mr-auto">
          <li className="nav-item">
            <Link to={"/home"} className="nav-link">
              Home
            </Link>
          </li>

          {showModeratorBoard && (
            <li className="nav-item">
              <Link to={"/mod"} className="nav-link">
                Moderator Board
              </Link>
            </li>
          )}

          {showAdminBoard && (
            <li className="nav-item">
              <Link to={"/admin"} className="nav-link">
                Admin Board
              </Link>
            </li>
          )}

          {currentUser && (
            <li className="nav-item">
              <Link to={"/user"} className="nav-link">
                User
              </Link>
            </li>
          )}
        </div>

        {currentUser ? (
          <div className="navbar-nav ml-auto">
            <li className="nav-item">
              <Link to={"/profile"} className="nav-link">
                {currentUser.username}
              </Link>
            </li>
            <li className="nav-item">
              <a href="/login" className="nav-link" onClick={logOut}>
                LogOut
              </a>
            </li>
          </div>
        ) : (
          <div className="navbar-nav ml-auto">
            <li className="nav-item">
              <Link to={"/login"} className="nav-link">
                Login
              </Link>
            </li>

            <li className="nav-item">
              <Link to={"/register"} className="nav-link">
                Sign Up
              </Link>
            </li>
          </div>
        )}
      </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;

Logout when Token is expired

There are two ways to handle JWT Token expiration.

Add CSS style for React Pages

Open src/App.css and write some CSS code as following:

label {
  display: block;
  margin-top: 10px;
}

.card-container.card {
  max-width: 350px !important;
  padding: 40px 40px;
}

.card {
  background-color: #f7f7f7;
  padding: 20px 25px 30px;
  margin: 0 auto 25px;
  margin-top: 50px;
  -moz-border-radius: 2px;
  -webkit-border-radius: 2px;
  border-radius: 2px;
  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}

.profile-img-card {
  width: 96px;
  height: 96px;
  margin: 0 auto 10px;
  display: block;
  -moz-border-radius: 50%;
  -webkit-border-radius: 50%;
  border-radius: 50%;
}

Configure Port for React JWT Auth Client with Web API

Because most of HTTP Server use CORS configuration that accepts resource sharing restricted to some sites or ports, so we also need to configure port for our App.

In project folder, create .env file with following content:

PORT=8081

Now we’ve set our app running at port 8081

Leave a Reply