Back

User registration and login with React and Axios

User registration and login with React and Axios

Most web applications nowadays require user authentication and authorization: one for validation and confirming that users are who they say they are, and the other for granting permissions and access to recognized users. A popular option is using a social login provider like Google or Facebook. Uniquely, we can build authentication with React, Axios, and a Node.js backend.

In this article, we will create a React login form that uses Axios to submit users’ information for authentication with key accessibilities to deliver success. This article is both beginner- and intermediate-level developer-friendly.

Prerequisites

For this tutorial, we will be focusing only on the frontend aspect of things, but here is a link to the GitHub repo, where I have a backend prepared for this tutorial. Clone it, run yarn install or npm install to install all dependencies, and then run npm run dev or yarn dev to start up the server, which will be running on http://localhost:3500/.

Next, create a .env file, which will store our environmental variables. Open the terminal and write Node, now, copy and paste the following into the prompt:

require("crypto").randomBytes(64).toString("hex");

This will create a unique token for you. Copy the token and in the .env file, create an ACCESS_TOKEN_SECRET=” Unique token goes here”. Next, create another token, and in the .env file, create a REFRESH_TOKEN_SECRET with the unique token.

Setting up a backend

With that, our backend is ready.

Creating a registration form

We will create a new React app with the command below:

npx create-react-app auth

Using yarn:

yarn create react-app auth

I will be using yarn from here on out, and you can name your application whatever you want. Next, create a registration file named register.jsx for handling the registration and inside, go ahead, copy and paste the code below:

const Register = () => {
  return (
    <>
      <section>
        <p
          ref={errRef}
          className={errMsg ? "errmsg" : "offscreen"}
          aria-live="assertive"
        >
          {errMsg}
        </p>
        <h1>Register</h1>
        <form onSubmit={handleSubmit}>
          <label htmlFor="username">
            Username:
            <FontAwesomeIcon
              icon={faCheck}
              className={validName ? "valid" : "hide"}
            />
            <FontAwesomeIcon
              icon={faTimes}
              className={
                validName || !user ? "hide" : "invalid"
              }
            />
          </label>
          <input
            type="text"
            id="username"
            ref={userRef}
            autoComplete="off"
            onChange={(e) => setUser(e.target.value)}
            value={user}
            required
            aria-invalid={validName ? "false" : "true"}
            aria-describedby="uidnote"
            onFocus={() => setUserFocus(true)}
            onBlur={() => setUserFocus(false)}
          />
          <p
            id="uidnote"
            className={
              userFocus && user && !validName
                ? "instructions"
                : "offscreen"
            }
          >
            <FontAwesomeIcon icon={faInfoCircle} />
            4 to 24 characters.
            <br />
            Must begin with a letter.
            <br />
            Letters, numbers, underscores, hyphens allowed.
          </p>
          <label htmlFor="password">
            Password:
            <FontAwesomeIcon
              icon={faCheck}
              className={validPwd ? "valid" : "hide"}
            />
            <FontAwesomeIcon
              icon={faTimes}
              className={
                validPwd || !pwd ? "hide" : "invalid"
              }
            />
          </label>
          <input
            type="password"
            id="password"
            onChange={(e) => setPwd(e.target.value)}
            value={pwd}
            required
            aria-invalid={validPwd ? "false" : "true"}
            aria-describedby="pwdnote"
            onFocus={() => setPwdFocus(true)}
            onBlur={() => setPwdFocus(false)}
          />
          <p
            id="pwdnote"
            className={
              pwdFocus && !validPwd
                ? "instructions"
                : "offscreen"
            }
          >
            <FontAwesomeIcon icon={faInfoCircle} />
            8 to 24 characters.
            <br />
            Must include uppercase and lowercase letters, a
            number and a special character.
            <br />
            Allowed special characters:{" "}
            <span aria-label="exclamation mark">
              !
            </span>{" "}
            <span aria-label="at symbol">@</span>{" "}
            <span aria-label="hashtag">#</span>{" "}
            <span aria-label="dollar sign">$</span>{" "}
            <span aria-label="percent">%</span>
          </p>
          <label htmlFor="confirm_pwd">
            Confirm Password:
            <FontAwesomeIcon
              icon={faCheck}
              className={
                validMatch && matchPwd ? "valid" : "hide"
              }
            />
            <FontAwesomeIcon
              icon={faTimes}
              className={
                validMatch || !matchPwd ? "hide" : "invalid"
              }
            />
          </label>
          <input
            type="password"
            id="confirm_pwd"
            onChange={(e) => setMatchPwd(e.target.value)}
            value={matchPwd}
            required
            aria-invalid={validMatch ? "false" : "true"}
            aria-describedby="confirmnote"
            onFocus={() => setMatchFocus(true)}
            onBlur={() => setMatchFocus(false)}
          />
          <p
            id="confirmnote"
            className={
              matchFocus && !validMatch
                ? "instructions"
                : "offscreen"
            }
          >
            <FontAwesomeIcon icon={faInfoCircle} />
            Must match the first password input field.
          </p>
          <button
            disabled={
              !validName || !validPwd || !validMatch
                ? true
                : false
            }
          >
            Sign Up
          </button>
        </form>
        <p>
          Already registered?
          <br />
          <span className="line">
            {/*put router link here*/}
            {/* <a href="#">Sign In</a> */}
          </span>
        </p>
      </section>
      )}
    </>
  );
};

export default Register;

In the code block above, we created a form for registration, and here is why you can’t authenticate users that are not registered. We created some state and ref events which we will create in the following sections. We also added Aria-labels for screen accessibility; you can read more on aria-labels here and here.

Creating and handling validation logic

We will create a regex for handling username values and one for handling password values.

const USER_REGEX = /^\[A-z\][A-z0-9-_]{3,23}$/;
const PWD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%]).{8,24}$/;

Next, we will create the state events and also the ref events. The ref events will be used to set a focus on the errors so it could display error characters if an error occurs so that the user can be notified for wrong inputs. This would be right before we return any HTML element in the component.

const userRef = useRef();
const errRef = useRef();
const [user, setUser] = useState("");
const [validName, setValidName] = useState(false);
const [userFocus, setUserFocus] = useState(false);
const [pwd, setPwd] = useState("");
const [validPwd, setValidPwd] = useState(false);
const [pwdFocus, setPwdFocus] = useState(false);
const [matchPwd, setMatchPwd] = useState("");
const [validMatch, setValidMatch] = useState(false);
const [matchFocus, setMatchFocus] = useState(false);
const [errMsg, setErrMsg] = useState("");
const [success, setSuccess] = useState(false);
// This will be used with the login

We will also create some useEffects to set the useRef focus and handle and check changes to our state events.

// State events
useEffect(() => {
  userRef.current.focus();
}, []);

useEffect(() => {
  setValidName(USER_REGEX.test(user));
}, [user]);

useEffect(() => {
  setValidPwd(PWD_REGEX.test(pwd));
  setValidMatch(pwd === matchPwd);
}, [pwd, matchPwd]);

useEffect(() => {
  setErrMsg("");
}, [user, pwd, matchPwd]);

Handling the submit function

We will install Axios, which will help send the registration data to our backend. It will be used in authenticating a user when they log in. Install Axios using the command below:

yarn add Axios

Create a folder within the src directory and name it api/axios.js. This folder contains a file that will be responsible for sending our registered data to our backend.

// axios.js
import axios from "axios";
export default axios.create({
  baseURL: "http://localhost:3500",
});

In the code above, we created a baseURL specifying the URL where our backend will exist. We then import it into our registration file and then use it to handle submitting the filled data of registered users to the backend.

// Register.js
const REGISTER_URL = "/register";
import axios from "./api/axios";
// other imports
const handleSubmit = async (e) => {
  e.preventDefault();
  const v1 = USER_REGEX.test(user);
  const v2 = PWD_REGEX.test(pwd);
  if (!v1 || !v2) {
    setErrMsg("Invalid Entry");
    return;
  }

  try {
    const response = await axios.post(
      REGISTER_URL,
      JSON.stringify({ user, pwd }),
      {
        headers: { "Content-Type": "application/json" },
        withCredentials: true,
      }
    );
    setSuccess(true);
    //clear state and controlled inputs
    setUser("");
    setPwd("");
    setMatchPwd("");
  } catch (err) {
    if (!err?.response) {
      setErrMsg("No Server Response");
    } else if (err.response?.status === 409) {
      setErrMsg("Username Taken");
    } else {
      setErrMsg("Registration Failed");
    }
    errRef.current.focus();
  }
};

We created a function for handling our submission in the code block above. Inside we checked to see if the username or password passed the regex test. We also created a registration URL that we can submit to the backend.

The register form

And, after submitting the data:

After registering

And with that, we are done creating the registration form. Let’s create a login form to test whether we can log in with the registered credentials.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Creating a Login form

Create a file inside the src folder and name it Login.js. Open the file, copy the code below and paste it inside:

// Login.js
import { useRef, useState, useEffect, useContext } from "react";
const Login = () => {
  const userRef = useRef();
  const errRef = useRef();
  const [user, setUser] = useState("");
  const [pwd, setPwd] = useState("");
  const [errMsg, setErrMsg] = useState("");
  const [success, setSuccess] = useState(false);
  useEffect(() => {
    userRef.current.focus();
  }, []);
  useEffect(() => {
    setErrMsg("");
  }, [user, pwd]);

  return (
    <>
      {success ? (
        <section>
          <h1>You are logged in!</h1>
          <br />
          <p>{/* <a href="#">Go to Home</a> */}</p>
        </section>
      ) : (
        <section>
          <p
            ref={errRef}
            className={errMsg ? "errmsg" : "offscreen"}
            aria-live="assertive"
          >
            {errMsg}
          </p>
          <h1>Sign In</h1>
          <form onSubmit={handleSubmit}>
            <label htmlFor="username">Username:</label>
            <input
              type="text"
              id="username"
              ref={userRef}
              autoComplete="off"
              onChange={(e) => setUser(e.target.value)}
              value={user}
              required
            />
            <label htmlFor="password">Password:</label>
            <input
              type="password"
              id="password"
              onChange={(e) => setPwd(e.target.value)}
              value={pwd}
              required
            />
            <button>Sign In</button>
          </form>
          <p>
            Need an Account?
            <br />
            <span className="line">
              <a href="#">Sign Up</a>
            </span>
          </p>
        </section>
      )}
    </>
  );
};

export default Login;

We created a login form in the code block above. We also created state and ref events to control our inputs.

Creating AuthContext

In this section, we will create a global Auth state and use the context API. Inside our src folder, we would create a folder called context-Api and a file right inside it called AuthProvider.js.

// AuthProvider.js
import { createContext, useState } from "react";
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
  const [auth, setAuth] = useState({});
  return (
    <AuthContext.Provider value={{ auth, setAuth }}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

In the code above, we defined our global state, which we will import into our index.js file and wrap our App component so that the entire app can have access to the auth state.

// index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { AuthProvider } from "./context/AuthProvider";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <AuthProvider>
      <App />
    </AuthProvider>
  </React.StrictMode>
);

Next, we import AuthContext into our login.js. We also created a handleSubmit function in charge of sending and verifying the data sent to the backend.

// Login.js
// imports
import AuthContext from "./context/AuthProvider";
function Login() {
  const { setAuth } = useContext(authContext);
  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await axios.post(
        LOGIN_URL,
        JSON.stringify({ user, pwd }),
        {
          headers: { "Content-Type": "application/json" },
          withCredentials: true,
        }
      );
      const accessToken = response?.data?.accessToken;
      const roles = response?.data?.roles;
      setAuth({ user, pwd, roles, accessToken });
      setUser("");
      setPwd("");
      setSuccess(true);
    } catch (err) {
      if (!err?.response) {
        setErrMsg("No Server Response");
      } else if (err.response?.status === 400) {
        setErrMsg("Missing Username or Password");
      } else if (err.response?.status === 401) {
        setErrMsg("Unauthorized");
      } else {
        setErrMsg("Login Failed");
      }
      errRef.current.focus();
    }
  };
}

You can add the following styles inside your index.js file.

@import url('https://fonts.googleapis.com/css2?family=Nunito&display=swap');
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  font-family: 'Nunito', sans-serif;
  font-size: 22px;
  color: #fff;
}

body {
  min-height: 100vh;
  background-color: rgb(255, 184, 30);
}

.App {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  padding: 1rem 0.5rem;
}

section {
  width: 100%;
  max-width: 420px;
  min-height: 400px;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  padding: 1rem;
  background-color: rgba(0, 0, 0, 0.4);
}

form {
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
  flex-grow: 1;
  padding-bottom: 1rem;
}

a,
a:visited {
  color: #fff;
}

input[type='text'],
input[type='password'],
button,
textarea {
  font-family: 'Nunito', sans-serif;
  font-size: 22px;
  padding: 0.25rem;
  border-radius: 0.5rem;
}

label,
button {
  margin-top: 1rem;
}

button {
  padding: 0.5rem;
}

.instructions {
  font-size: 0.75rem;
  border-radius: 0.5rem;
  background: #000;
  color: #fff;
  padding: 0.25rem;
  position: relative;
  bottom: -10px;
}

.instructions>svg {
  margin-right: 0.25rem;
}

.offscreen {
  position: absolute;
  left: -9999px;
}

.hide {
  display: none;
}

.valid {
  color: limegreen;
  margin-left: 0.25rem;
}

.invalid {
  color: red;
  margin-left: 0.25rem;
}

.errmsg {
  background-color: lightpink;
  color: firebrick;
  font-weight: bold;
  padding: 0.5rem;
  margin-bottom: 0.5rem;
}

.line {
  display: inline-block;
}

This would be the result:

The sign-in form

And, after clicking in the button:

After successfully logging in

Conclusion

In this article, we learned about User Login and authentication using React18 and Axios. We also learned the basic way to authenticate with a Node.js server. Here is the git repo for the React project.