Clue Mediator

Login form example with API using React Hooks

πŸ“…November 24, 2022
πŸ—ReactJS

In this article, we will show you how to create a simple login form example with API using React Hooks. We created a similar example using react class component but here we will use React Hooks and routing-v6" title="Routing v6">react-router-dom v6.

In this demonstration, we'll carry out backend activities using the Node.js API that was previously developed. Therefore, we will concentrate on front-end implementation.

Demo Application

<a href=Login App">

Login App

Steps to create login form example with API using React Hooks

  1. Project structure
  2. Package version
  3. Setup secure REST API">Setup secure REST API
  4. Create functional components and route them
  5. Setup authenticated routes
  6. Output

1. Project structure

Refer to the following project structure for login example.

  • login-app-react-hooks

    • node_modules

    • public

      • index.html
    • src

      • pages

        • Dashboard.js
        • Home.js
        • Login.js
        • NotFound.js
      • utils

        • common.js
        • PrivateRoutes.js
        • PublicRoutes.js
      • App.js

      • index.css

      • index.js

    • package-lock.json

    • package.json

    • README.md

2. Package version

In this example, we have used the following packages.

react

^18.2.0

react-router-dom

^6.3.0

axios

^1.2.0

3. Setup secure REST API

To create a login form, we need secure REST API to integrate into the application. So we have already created the REST API in Node.js for authentication. Kindly refer the below article to create it or clone it from the GitHub repository.

jwt" title="Login App – Create REST API for authentication in Node.js using JWT">Login App – Create REST API for authentication in Node.js using JWT

After that just run the project so we can consume the REST API. It will execute on port 4000.

4. Create functional components and route them

Now, It’s time to create components for Home, Login and Dashboard pages and link it using the routing.

  • Home component (Access it with/without login) – It’s a simple component which we can access it with/without user login. By default we’ll open this component.
  • Login component (Access it without login only) – Here we will create a login page. So with the help of it we can call the REST API and on successful call we will redirect it on the Dashboard component.
  • Dashboard component (Access it after login only) – It’s a private page that users can access only after successfully logged in. We will also provide the option to logout from user account in the same component.

If you don’t know how to implement react-router-dom v6 in react app then refer to the following link for more information.

Routing in React using React Router v6

For demo purpose we are creating these components and updating the css in "src" directory.

src/pages/Home.js

import React from 'react';

const Home = () => {
  return <div>
    Welcome to the Home Page!
  </div>
}

export default Home;

src/pages/Login.js

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';

const Login = props => {
  const history = useNavigate();
  const username = useFormInput('');
  const password = useFormInput('');
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  // handle button click of login form
  const handleLogin = () => {
    history('/dashboard');
  }

  return (
    <div>
      Login<br><br>
      <div>
        Username<br>
        <input type="text" {...username}="" autocomplete="new-password">
      </div>
      <div 10="" style={{ margintop: }}>
        Password<br>
        <input type="password" {...password}="" autocomplete="new-password">
      </div>
      {error && <><small style={{ color: 'red' }}>{error}</small><br>}<br>
      <input type="button" value="{loading" ?="" 'loading...'="" :="" 'login'}="" onclick={handleLogin} disabled><br>
    </div>
  );
}

const useFormInput = initialValue => {
  const [value, setValue] = useState(initialValue);

  const handleChange = e => {
    setValue(e.target.value);
  }
  return {
    value,
    onChange: handleChange
  }
}

export default Login;

src/pages/Dashboard.js

import React from 'react';
import { useNavigate } from 'react-router-dom';

const Dashboard = props => {
  const history = useNavigate();

  // handle click event of logout button
  const handleLogout = () => {
    history('/login');
  }

  return (
    <div>
      Welcome User!<br><br>
      <input type="button" onclick={handleLogout} value="Logout">
    </div>
  );
}

export default Dashboard;

src/pages/NotFound.js

import React from 'react';

const NotFound = () => {
  return <div>
    <h2 0="" style={{ marginbottom: }}>404</h2>
    <h4 0="" style={{ margintop: }}>Page Not Found!</h4>
  </div>
}

export default NotFound;

src/App.js

import React from 'react';
import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom';

import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import Home from './pages/Home';
import NotFound from './pages/NotFound';

function App() {
  return (
    <browserrouter>
      <div>
        <div class="header">
          <navlink class="{({" isactive="" })=""> isActive ? 'active' : ''} to="/">Home</navlink>
          <navlink class="{({" isactive="" })=""> isActive ? 'active' : ''} to="/login">Login</navlink><small>(Access without token only)</small>
          <navlink class="{({" isactive="" })=""> isActive ? 'active' : ''} to="/dashboard">Dashboard</navlink><small>(Access with token only)</small>
        </div>
        <div class="content">
          <routes>
            <route path="*" element="{<NotFound">} />
            <route index="" element="{<Home">} />
            <route path="/login" element="{<Login">} />
            <route path="/dashboard" element="{<Dashboard">} />
          </route></route></route></route></routes>
        </div>
      </div>
    </browserrouter>
  );
}

export default App;

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <react class="strictmode">
    <app>
  </app></react>
);

src/index.css

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
    "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
    monospace;
}

.content {
  padding: 20px;
}

.header {
  padding: 10px;
  background: #edf2f4;
  border-bottom: 1px solid #999;
}
.header a {
  color: #0072ff;
  text-decoration: none;
  margin-left: 20px;
  margin-right: 5px;
}
.header a:hover {
  color: #8a0f53;
}
.header small {
  color: #666;
}
.header .active {
  color: #2c7613;
}

5. Steup authenticated routes

Let's integrate the secure API to the react application. Follow the below steps to do it.

  • Create common utils

    To manage the login and logout functionality, we need to create a few functions in β€œcommon.js” file which will help us to manage token and user data using `sessionStorage`. Here we will create this file in β€œsrc/utils” directory.

src/utils/common.js

// return the user data from the session storage
export const getUser = () => {
  const userStr = sessionStorage.getItem('user');
  if (userStr) return JSON.parse(userStr);
  else return null;
}

// return the token from the session storage
export const getToken = () => {
  return sessionStorage.getItem('token') || null;
}

// remove the token and user from the session storage
export const removeUserSession = () => {
  sessionStorage.removeItem('token');
  sessionStorage.removeItem('user');
}

// set the token and user from the session storage
export const setUserSession = (token, user) => {
  sessionStorage.setItem('token', token);
  sessionStorage.setItem('user', JSON.stringify(user));
}
  • Integrate sign in API

    Let's integrate the "/users/signin" API on the click event of the login button. Refer to the below link if you don't know how to call API in ReactJS.

    API call in React JS

    Here, we used the axios for the API call. After successful login, we are storing the response data in sessionStorage and redirect it to the dashboard page. Also managing the loading and error flag via `useState`.

src/pages/Login.js

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { setUserSession } from '../utils/common';
import axios from 'axios';

const Login = props => {
  const history = useNavigate();
  const username = useFormInput('');
  const password = useFormInput('');
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  // handle button click of login form
  const handleLogin = () => {
    setError(null);
    setLoading(true);
    axios.post('http://localhost:4000/users/signin', { username: username.value, password: password.value }).then(response => {
      setLoading(false);
      setUserSession(response.data.token, response.data.user);
      history('/dashboard');
    }).catch(error => {
      setLoading(false);
      if (error.response.status === 401) setError(error.response.data.message);
      else setError("Something went wrong. Please try again later.");
    });
  }

  return (
    <div>
      Login<br><br>
      <div>
        Username<br>
        <input type="text" {...username}="" autocomplete="new-password">
      </div>
      <div 10="" style={{ margintop: }}>
        Password<br>
        <input type="password" {...password}="" autocomplete="new-password">
      </div>
      {error && <><small style={{ color: 'red' }}>{error}</small><br>}<br>
      <input type="button" value="{loading" ?="" 'loading...'="" :="" 'login'}="" onclick={handleLogin} disabled><br>
    </div>
  );
}

const useFormInput = initialValue => {
  const [value, setValue] = useState(initialValue);

  const handleChange = e => {
    setValue(e.target.value);
  }
  return {
    value,
    onChange: handleChange
  }
}

export default Login;
  • Manage the logout in dashboard

    On click of the Logout button, we are removing the data from the sessionStorage and redirect it to the login page. Also we are showing the user name on page of the Dashboard.

src/pages/Dashboard.js

import React from 'react';
import { useNavigate } from 'react-router-dom';
import { getUser, removeUserSession } from '../utils/common';

const Dashboard = props => {
  const history = useNavigate();
  const user = getUser();

  // handle click event of logout button
  const handleLogout = () => {
    removeUserSession();
    history('/login');
  }

  return (
    <div>
      Welcome {user.name}!<br><br>
      <input type="button" onclick={handleLogout} value="Logout">
    </div>
  );
}

export default Dashboard;
  • Create public and private routes

We divided routes in three parts.

  • Normal routes – Which we can use to access routes with or without user login.
  • Public routes – Which we can use to access the routes without login token only. So if user is already logged-in then we will redirect it to the dashboard page.
  • Private routes – Which we can use to access the routes with login token only. So if user is not logged-in then we will redirect back it to the login page.

src/utils/PrivateRoutes.js

import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import { getToken } from './common';

// handle the private routes
const PrivateRoutes = () => {
  return getToken() ? <outlet> : <navigate to="/login">
}

export default PrivateRoutes;
</navigate></outlet>

src/utils/PublicRoutes.js

import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import { getToken } from './common';

// handle the public routes
const PublicRoutes = () => {
  return !getToken() ? <outlet> : <navigate to="/dashboard">
}

export default PublicRoutes;
</navigate></outlet>

Now we have to slightly update the routes in "App.js" to implement authentication.

src/App.js

import PrivateRoutes from './utils/PrivateRoutes';
import PublicRoutes from './utils/PublicRoutes';

...
...

function App() {
 ...
 ...
  return (
    <browserrouter>
      ...
      ...
      <routes>
        <route path="*" element="{<NotFound">} />
        <route index="" element="{<Home">} />
        <route element="{<PublicRoutes">}>
          <route path="/login" element="{<Login">} />
        </route>
        <route element="{<PrivateRoutes">}>
          <route path="/dashboard" element="{<Dashboard">} />
        </route>
      </route></route></route></route></routes>
    </browserrouter>
  );
}

export default App;
  • Handle the browser refresh

    At last, we have to handle the browser refresh to authenticate the application. For that we will integrate one more API called "/verifyToken" and manage the authentication flag. Based on the authentication flag we will manage the loading screen.

src/App.js

import React, { useState, useEffect } from 'react';
import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom';
import axios from 'axios';

import PrivateRoutes from './utils/PrivateRoutes';
import PublicRoutes from './utils/PublicRoutes';
import { getToken, removeUserSession, setUserSession } from './utils/common';

import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import Home from './pages/Home';
import NotFound from './pages/NotFound';

function App() {
  const [authLoading, setAuthLoading] = useState(true);

  useEffect(() => {
    const token = getToken();
    if (!token) {
      return;
    }

    axios.get(`http://localhost:4000/verifyToken?token=${token}`).then(response => {
      setUserSession(response.data.token, response.data.user);
      setAuthLoading(false);
    }).catch(error => {
      removeUserSession();
      setAuthLoading(false);
    });
  }, []);

  if (authLoading && getToken()) {
    return <div class="content">Checking Authentication...</div>
  }

  return (
    <browserrouter>
      <div class="header">
        <navlink class="{({" isactive="" })=""> isActive ? 'active' : ''} to="/">Home</navlink>
        <navlink class="{({" isactive="" })=""> isActive ? 'active' : ''} to="/login">Login</navlink><small>(Access without token only)</small>
        <navlink class="{({" isactive="" })=""> isActive ? 'active' : ''} to="/dashboard">Dashboard</navlink><small>(Access with token only)</small>
      </div>
      <div class="content">
        <routes>
          <route path="*" element="{<NotFound">} />
          <route index="" element="{<Home">} />
          <route element="{<PublicRoutes">}>
            <route path="/login" element="{<Login">} />
          </route>
          <route element="{<PrivateRoutes">}>
            <route path="/dashboard" element="{<Dashboard">} />
          </route>
        </route></route></route></route></routes>
      </div>
    </browserrouter>
  );
}

export default App;

6. Output

Run the login application and check the output in the browser.

I hope you find this article helpful.
Thank you for reading. Happy Coding..!! πŸ™‚

Demo & Source Code

GitHub Repository