Building a React Login Page Template

Category
Guides
Published

Discover how to create a secure login page for your React app with authentication, input validation, and styling.

React is a popular open source JavaScript library for building frontend applications and user interfaces. It offers a structured way of building web apps while retaining complete flexibility over the toolchains and integrations with other frameworks or APIs.

Simple to get to grips with and lightweight, React offers a range of benefits to developers—it's highly performant (reinforced by virtual DOM), it has reusable components that facilitate the quick scaffolding of large and complex apps, and last but not least, it offers solid support, including comprehensive documentation and a huge and active community of React developers.

One of the most integral parts of modern web applications is authentication, which not only helps you establish the identity of your users but also facilitates the implementation of roles and privileges to offer more control while maintaining security and abstraction.

However, implementing authentication in any web app from scratch can be a complex process. In this article, you'll see how to implement authentication in a React app manually by building a login page then adding input validation and styling. You'll then learn how to do it quicker by using Clerk to avoid the hassle of building the whole login flow from scratch.

Building a React Login Page

In this section, you'll see how to develop a custom React login page manually as well as add styling and validation. Once you get to the part where you need to add authentication, you'll make use of an Express-based auth server that uses JWT tokens to authenticate users. Finally, you'll see how to do the same using Clerk to avoid having to manually design the authentication flow.

The complete code for this tutorial can be found in this GitHub repo.

Prerequisites

To follow along with this tutorial, you need only Node.js and npm, the standard package manager for JavaScript.

Once you have these set up, start by creating a new directory for the project:

mkdir react-login-demo
cd react-login-demo

Next, create a new React app by running the following command:

npx create-react-app frontend

Once the app is ready, you'll need to install react-router to help set up page routing. You can do this by running the following commands:

# Change your working directory
cd frontend

# Install the package
npm i react-router-dom

# Change your working directory back to the project root
cd ..

You'll also need a basic auth server to set up authentication in your app. You'll need to create an Express app in Node.js, which you can do by running the following commands:

# Create a new directory for the server
mkdir auth-server

# Change the working directory to the newly created directory
cd auth-server

# Initialize a new Node app
npm init -y

# Install required dependencies
npm i express cors bcrypt jsonwebtoken lowdb

# Change your working directory to the React app to continue with the tutorial
cd ../frontend

The auth server will use the following dependencies to set up JWT-based authentication:

  • bcrypt: for hashing and comparing passwords
  • jsonwebtoken: for generating and verifying JSON web tokens
  • lowdb: for storing user details (email and hashed password)

You can now proceed with creating the React application.

Create a Welcome Page

First, you'll need a welcome page for your home route (http://localhost:3000/). You can use the following code snippet by saving it in a file called home.js:

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

const Home = (props) => {
  const { loggedIn, email } = props
  const navigate = useNavigate()

  const onButtonClick = () => {
    // You'll update this function later
  }

  return (
    <div className="mainContainer">
      <div className={'titleContainer'}>
        <div>Welcome!</div>
      </div>
      <div>This is the home page.</div>
      <div className={'buttonContainer'}>
        <input
          className={'inputButton'}
          type="button"
          onClick={onButtonClick}
          value={loggedIn ? 'Log out' : 'Log in'}
        />
        {loggedIn ? <div>Your email address is {email}</div> : <div />}
      </div>
    </div>
  )
}

export default Home

Update the code in App.js to set up page routing and add the home route. Also, add a login route, which will house the login page:

import { BrowserRouter, Route, Routes } from 'react-router-dom'
import Home from './home'
import Login from './login'
import './App.css'
import { useEffect, useState } from 'react'

function App() {
  const [loggedIn, setLoggedIn] = useState(false)
  const [email, setEmail] = useState('')

  return (
    <div className="App">
      <BrowserRouter>
        <Routes>
          <Route
            path="/"
            element={<Home email={email} loggedIn={loggedIn} setLoggedIn={setLoggedIn} />}
          />
          <Route path="/login" element={<Login setLoggedIn={setLoggedIn} setEmail={setEmail} />} />
        </Routes>
      </BrowserRouter>
    </div>
  )
}

export default App

Once the home page is ready, it will look like this:

Home page logged out

It will render a Log in button if you're not logged in, which upon clicking will lead you to a login page (which you'll create in the next step).

If you're logged in, it will render a Log out button with your email address, as displayed below:

Home page logged in

Create a Login Page

Once your home page is ready, the next step is to create a login page. In this section, you will create and style a login page.

Create a Simple Login Page

To get started, save the following code snippet in login.js:

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

const Login = (props) => {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [emailError, setEmailError] = useState('')
  const [passwordError, setPasswordError] = useState('')

  const navigate = useNavigate()

  const onButtonClick = () => {
    // You'll update this function later...
  }

  return (
    <div className={'mainContainer'}>
      <div className={'titleContainer'}>
        <div>Login</div>
      </div>
      <br />
      <div className={'inputContainer'}>
        <input
          value={email}
          placeholder="Enter your email here"
          onChange={(ev) => setEmail(ev.target.value)}
          className={'inputBox'}
        />
        <label className="errorLabel">{emailError}</label>
      </div>
      <br />
      <div className={'inputContainer'}>
        <input
          value={password}
          placeholder="Enter your password here"
          onChange={(ev) => setPassword(ev.target.value)}
          className={'inputBox'}
        />
        <label className="errorLabel">{passwordError}</label>
      </div>
      <br />
      <div className={'inputContainer'}>
        <input className={'inputButton'} type="button" onClick={onButtonClick} value={'Log in'} />
      </div>
    </div>
  )
}

export default Login

This will create a simple login page with two fields—email and password—and a login button. However, you'll also need to add some styling to this page to make it look better.

Add Styling to Your Login Page

To add some basic styling with CSS, paste the following code snippet in App.css:

.mainContainer {
  flex-direction: column;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
}

.titleContainer {
  display: flex;
  flex-direction: column;
  font-size: 64px;
  font-weight: bolder;
  align-items: center;
  justify-content: center;
}

.resultContainer,
.historyItem {
  flex-direction: row;
  display: flex;
  width: 400px;
  align-items: center;
  justify-content: space-between;
}

.historyContainer {
  flex-direction: column;
  display: flex;
  height: 200px;
  align-items: center;
  flex-grow: 5;
  justify-content: flex-start;
}

.buttonContainer {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 260px;
}

.inputContainer {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
}

.inputContainer > .errorLabel {
  color: red;
  font-size: 12px;
}

.inputBox {
  height: 48px;
  width: 400px;
  font-size: large;
  border-radius: 8px;
  border: 1px solid grey;
  padding-left: 8px;
}

Also, update the styles in index.css with the snippet below:

html,
body {
  padding: 0;
  margin: 0;
  font-family:
    -apple-system,
    BlinkMacSystemFont,
    Segoe UI,
    Roboto,
    Oxygen,
    Ubuntu,
    Cantarell,
    Fira Sans,
    Droid Sans,
    Helvetica Neue,
    sans-serif;
}

* {
  box-sizing: border-box;
}

main {
  padding: 5rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

code {
  background: #fafafa;
  border-radius: 5px;
  padding: 0.75rem;
  font-family:
    Menlo,
    Monaco,
    Lucida Console,
    Courier New,
    monospace;
}

input[type='button'] {
  border: none;
  background: cornflowerblue;
  color: white;
  padding: 12px 24px;
  margin: 8px;
  font-size: 24px;
  border-radius: 8px;
  cursor: pointer;
}

This is how the login page should look like:

Login page styled

The next step is to add some basic validation to the login form.

Add Validation

You'll notice that the code for the login form already contains <label> components right below the <input> components. You'll use these labels to show validation errors to users. The labels have already been styled in the stylesheets you saved above.

All you need to do now is to write the validation logic. Update the onButtonClick function in the login.js file with the following code:

const onButtonClick = () => {
  // Set initial error values to empty
  setEmailError('')
  setPasswordError('')

  // Check if the user has entered both fields correctly
  if ('' === email) {
    setEmailError('Please enter your email')
    return
  }

  if (!/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(email)) {
    setEmailError('Please enter a valid email')
    return
  }

  if ('' === password) {
    setPasswordError('Please enter a password')
    return
  }

  if (password.length < 7) {
    setPasswordError('The password must be 8 characters or longer')
    return
  }

  // Authentication calls will be made here...
}

You can run the app with the following command:

npm start

You will now see validation errors pop up when you enter invalid input in the form.

Validation error

This completes the frontend setup for the app. Next, you'll set up the auth server that will create and verify users with JWT tokens, and finally, you'll connect the auth server and the React app to see the system in action.

Set Up Authentication

The auth server will contain four API routes:

  • GET at /: a basic home route with some info about the API
  • POST at /auth: an endpoint that will create and log in users and issue JSON web tokens
  • POST at /verify: an endpoint that will help verify JSON web tokens to see if they are valid
  • POST at /check-account: an endpoint that will check if a given email address has an associated entry in the auth database

Create an app.js file in your auth-server directory. Before writing the code for these endpoints, you will need to add the following imports and other boilerplate code to your newly created app.js file:

const express = require('express')
const bcrypt = require('bcrypt')
var cors = require('cors')
const jwt = require('jsonwebtoken')
var low = require('lowdb')
var FileSync = require('lowdb/adapters/FileSync')
var adapter = new FileSync('./database.json')
var db = low(adapter)

// Initialize Express app
const app = express()

// Define a JWT secret key. This should be isolated by using env variables for security
const jwtSecretKey = 'dsfdsfsdfdsvcsvdfgefg'

// Set up CORS and JSON middlewares
app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

You will also need to create a database.json file with the following contents in your auth-server directory:

{
  "users": []
}

Now, you can start creating each of the four endpoints.

  1. The first endpoint (/) is fairly simple:
// Basic home route for the API
app.get('/', (_req, res) => {
  res.send('Auth API.\nPlease use POST /auth & POST /verify for authentication')
})
  1. The /auth endpoint is slightly more complex:
// The auth endpoint that creates a new user record or logs a user based on an existing record
app.post('/auth', (req, res) => {
  const { email, password } = req.body

  // Look up the user entry in the database
  const user = db
    .get('users')
    .value()
    .filter((user) => email === user.email)

  // If found, compare the hashed passwords and generate the JWT token for the user
  if (user.length === 1) {
    bcrypt.compare(password, user[0].password, function (_err, result) {
      if (!result) {
        return res.status(401).json({ message: 'Invalid password' })
      } else {
        let loginData = {
          email,
          signInTime: Date.now(),
        }

        const token = jwt.sign(loginData, jwtSecretKey)
        res.status(200).json({ message: 'success', token })
      }
    })
    // If no user is found, hash the given password and create a new entry in the auth db with the email and hashed password
  } else if (user.length === 0) {
    bcrypt.hash(password, 10, function (_err, hash) {
      console.log({ email, password: hash })
      db.get('users').push({ email, password: hash }).write()

      let loginData = {
        email,
        signInTime: Date.now(),
      }

      const token = jwt.sign(loginData, jwtSecretKey)
      res.status(200).json({ message: 'success', token })
    })
  }
})
  1. The /verify endpoint simply checks for the validity of the supplied JWT token:
// The verify endpoint that checks if a given JWT token is valid
app.post('/verify', (req, res) => {
  const tokenHeaderKey = 'jwt-token'
  const authToken = req.headers[tokenHeaderKey]
  try {
    const verified = jwt.verify(authToken, jwtSecretKey)
    if (verified) {
      return res.status(200).json({ status: 'logged in', message: 'success' })
    } else {
      // Access Denied
      return res.status(401).json({ status: 'invalid auth', message: 'error' })
    }
  } catch (error) {
    // Access Denied
    return res.status(401).json({ status: 'invalid auth', message: 'error' })
  }
})
  1. The /check-account endpoint queries the database to see if an entry exists:
// An endpoint to see if there's an existing account for a given email address
app.post('/check-account', (req, res) => {
  const { email } = req.body

  console.log(req.body)

  const user = db
    .get('users')
    .value()
    .filter((user) => email === user.email)

  console.log(user)

  res.status(200).json({
    status: user.length === 1 ? 'User exists' : 'User does not exist',
    userExists: user.length === 1,
  })
})

Finally, add the following line to start the server:

app.listen(3080)

You can now run the following command in the auth-server directory to start the auth server:

node app.js

Make API Calls to the Server from the Frontend

The final step before you can see your React login system in action is to make API calls from the React app.

Update the onButtonClick function in login.js in the frontend/src directory to add the following call at the end (right after the validation code):

const onButtonClick = () => {
  // ... Validation code here ...

  // Check if email has an account associated with it
  checkAccountExists((accountExists) => {
    // If yes, log in
    if (accountExists) logIn()
    // Else, ask user if they want to create a new account and if yes, then log in
    else if (
      window.confirm(
        'An account does not exist with this email address: ' +
          email +
          '. Do you want to create a new account?',
      )
    ) {
      logIn()
    }
  })
}

Define the checkAccountExists and logIn functions below the onButtonClick function:

// Call the server API to check if the given email ID already exists
const checkAccountExists = (callback) => {
  fetch('http://localhost:3080/check-account', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email }),
  })
    .then((r) => r.json())
    .then((r) => {
      callback(r?.userExists)
    })
}

// Log in a user using email and password
const logIn = () => {
  fetch('http://localhost:3080/auth', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email, password }),
  })
    .then((r) => r.json())
    .then((r) => {
      if ('success' === r.message) {
        localStorage.setItem('user', JSON.stringify({ email, token: r.token }))
        props.setLoggedIn(true)
        props.setEmail(email)
        navigate('/')
      } else {
        window.alert('Wrong email or password')
      }
    })
}

Next, add a useEffect call in the App.js component to check if a user is logged in when the app loads:

useEffect(() => {
  // Fetch the user email and token from local storage
  const user = JSON.parse(localStorage.getItem('user'))

  // If the token/email does not exist, mark the user as logged out
  if (!user || !user.token) {
    setLoggedIn(false)
    return
  }

  // If the token exists, verify it with the auth server to see if it is valid
  fetch('http://localhost:3080/verify', {
    method: 'POST',
    headers: {
      'jwt-token': user.token,
    },
  })
    .then((r) => r.json())
    .then((r) => {
      setLoggedIn('success' === r.message)
      setEmail(user.email || '')
    })
}, [])

Note: Make sure to add import { useEffect } from 'react'; at the top of the file to import the method.

Finally, update the onButtonClick function in the home.js function to correctly handle login and logout cases:

const onButtonClick = () => {
  if (loggedIn) {
    localStorage.removeItem('user')
    props.setLoggedIn(false)
  } else {
    navigate('/login')
  }
}

This completes the setup of basic JWT-based auth in a React app. Let's test it out in the next section.

Test the App

Navigate to the home page of the app by opening http://localhost:3000 in your web browser.

Home page

Note: Make sure that both your frontend and your auth server are running.

You should see the home page of the app. Click Log in to be taken to the login page. Enter any email and a password (minimum of eight characters) and click Log in. If successful, you'll be redirected to the home page, and your email will be displayed below the Logout button.

Login flow

Note: The password as shown on the above screen is for demonstration purposes only. When building a login form in a real-world app, make sure to add type="password" in the <input> tag's attributes to mask the password characters entered by the user.

Click Log out to log out of the app. Before doing so, you can also try refreshing the app or navigating to the app in a separate tab/window to see if your user session is retained successfully.

You've now learned how to set up auth using React and an auth server written in Node.js and Express. However, this method was quite complex and needed a lot of manual design and implementation to get things right. And there are additional considerations when deploying and hosting your auth server to avoid security issues that this tutorial hasn't covered.

In the next section, you'll see a much simpler method of setting up authentication in your React app using Clerk.

Set Up React Authentication Using Clerk

Setting up authentication in a React app with Clerk is quite straightforward and friction-free. The following sections will help you get started with Clerk in the same React app you created above.

Set Up a Clerk Account and a Clerk App

Before getting started with the code, you'll need an account with Clerk. Create a new Clerk account if you don't have one already.

Once you're logged in, head over to the Clerk dashboard and click Add application.

Clerk dashboard

On the next page, enter a name for your app (you could go with something like "React Login Demo") and click Finish, leaving the Email address and Google switches turned on.

Finish creating application

Once the app is created, you'll be redirected to the app details in the Clerk dashboard. Click on the API Keys option under Developers in the left navigation pane.

App details

This is where you'll find your Clerk app's publishable key. You'll need this key to integrate and use the Clerk SDK in your React app. Copy and store it as a .env file in your React app's root directory.

Publishable API key

Now you can go ahead and set up the Clerk SDK in your React app.

Install Clerk Dependencies and Configure Login Page

To get started with Clerk in your React app, install the Clerk SDK by running the following command:

npm install @clerk/clerk-react

Make sure you've stored the publishable API key in a .env file at the root of your frontend project.

REACT_APP_CLERK_PUBLISHABLE_KEY=pk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Now, you can import and use the components provided by Clerk to easily set up authentication.

To begin, wrap the root component of your app (which you'll find in the index.js file) with the ClerkProvider component. Here's how your index.js file should look like when done:

import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'
import { ClerkProvider } from '@clerk/clerk-react'

const clerkPubKey = process.env.REACT_APP_CLERK_PUBLISHABLE_KEY

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <ClerkProvider publishableKey={clerkPubKey}>
    <App />
  </ClerkProvider>,
)

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

Next, head over to the home.js file and update it to look like the following:

import React from 'react'
import { SignedIn, SignedOut, SignInButton, SignOutButton, useUser } from '@clerk/clerk-react'

const Home = (props) => {
  // Use the useUser hook to get the details about the logged in user
  const { user } = useUser()

  return (
    <div className="mainContainer">
      <div className={'titleContainer'}>
        <div>Welcome!</div>
      </div>
      <div>This is the home page.</div>
      {/* The children of the SignedOut component are rendered only when the user is signed out from the app. In this case, the app will render a SignInButton */}
      <SignedOut>
        <SignInButton>
          <input className={'inputButton'} type="button" value={'Log in'} />
        </SignInButton>
      </SignedOut>

      {/* The children of the SignedIn component are rendered only when the user is signed in. In this case, the app will render the SignOutButton */}
      <SignedIn>
        <SignOutButton>
          <input className={'inputButton'} type="button" value={'Log out'} />
        </SignOutButton>
      </SignedIn>

      {/* You can also check if a user is logged in or not using the 'user' object from the useUser hook. In this case, a non-undefined user object will render the user's email on the page */}
      {user ? <div>Your email address is {user.primaryEmailAddress.emailAddress}</div> : null}
    </div>
  )
}

export default Home

That's it! That's how simple it is to set up auth using Clerk.

To clean up your app, head to the App.js file and remove the useEffect call that checks for the existing user using the old, basic method. You could also remove the /login route as the Clerk method has no use for it.

Testing the Clerk Method

To try out the Clerk method, start the React app using npm start (stop and start it if it's running already so that the env value is loaded correctly). You'll see the same Log in button as before.

Once you click it, you'll be redirected to the Clerk sign-in page, where you can log in using an email or your Google account. Once you've logged in, you will be redirected back to the home page of your app, and the logged-in user's email will be shown.

Clerk login demo

As you can see, setting up authentication with Clerk is far easier and simpler than the manual approach. You don't need to worry about maintaining user data in your database, and you can easily integrate third-party login methods (such as Google, Facebook, etc.) without having to set up their SDKs individually. The auth components offered by Clerk are tailored to make lives easier for React developers.

Once again, the code for this tutorial can be found in this GitHub repo.

Conclusion

React is one of the most versatile JavaScript-based libraries for building frontend web applications. In this article, you saw how to create a login page in a React app, add input validation, style the page, and set up an auth server in Node.js to handle authentication using JWT. Next, you saw how to do the same using Clerk, a modern user management platform built specifically for React and Next.js.

Clerk makes authentication and user management simple. It offers a wide range of customizable components to quickly integrate into your app. You can also use the hosted sign-in flow, as you saw in this article, to save yourself the effort of building and maintaining a sign-in flow. Make sure to sign up for Clerk to try it out in your React apps.

Ready to get started?

Start Building
Author
Kumar Harsh