Courses

React.js Client Parking App: Step-by-Step

Login page

Developing a login page has become a straightforward task, as we have already established all the necessary features for our application. It will closely resemble the registration page.

Login page

When a user logs in we want him to be redirected to the active parking list, so let's create a new empty component for this purpose, we will implement the active parking list later.

Login redirect

  1. Create a new src/views/parkings/ActiveParkings.jsx component.
function ActiveParkings() {
return <div>There will be active parkings list</div>
}
 
export default ActiveParkings
  1. Add a new named route 'parkings.active': '/parkings/active' to the src/routes/index.jsx file.
const routeNames = {
'home': '/',
'register': '/register',
'login': '/login',
'vehicles.index': '/vehicles',
'parkings.active': '/parkings/active',
}
  1. Define a new route for React Router in the src/main.jsx file.
import ActiveParkings from '@/views/parkings/ActiveParkings'
<Route path={ route('parkings.active') } element={<ActiveParkings />} />

The src/main.jsx file now should look like this.

import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import axios from 'axios'
import App from '@/App'
import Home from '@/views/Home'
import Register from '@/views/auth/Register'
import Login from '@/views/auth/Login'
import VehiclesList from '@/views/vehicles/VehiclesList'
import ActiveParkings from '@/views/parkings/ActiveParkings'
import '@/assets/main.css'
import { route } from '@/routes'
 
window.axios = axios
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
window.axios.defaults.withCredentials = true
window.axios.defaults.baseURL = 'http://parkingapi.test/api/v1'
 
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path={ route('home') } element={<App />}>
<Route index element={<Home />} />
<Route path={ route('register') } element={<Register />} />
<Route path={ route('login') } element={<Login />} />
<Route path={ route('vehicles.index') } element={<VehiclesList />} />
<Route path={ route('parkings.active') } element={<ActiveParkings />} />
</Route>
</Routes>
</BrowserRouter>
</React.StrictMode>,
)
  1. Update the leftAuthLinks function in the src/App.jsx function by adding a navigation link to the parkings.active route for authenticated users before vehicles.index.
function leftAuthLinks() {
return <>
<NamedLink name="parkings.active">
Parkings
</NamedLink>
<NamedLink name="vehicles.index">
Vehicles
</NamedLink>
</>
}
  1. Extend the src/hooks/useAuth.jsx hook by adding a new function to send login requests.
async function login(data) {
setErrors({})
setLoading(true)
 
return axios.post('/auth/login', data)
.then(response => {
setAccessToken(response.data.access_token)
navigate(route('parkings.active'))
})
.catch(error => {
if (error.response.status === 422) {
setErrors(error.response.data.errors)
}
})
.finally(() => setLoading(false))
}

It is almost identical to the register function, and the structure was explained in previous lessons. Now it has a different axios.post() URL.

And add it to the return statement.

return { register, login, errors, loading, isLoggedIn, logout }

The src/hooks/useAuth.jsx file now should have the following content.

import { useState, useMemo, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useLocalStorage } from 'react-use-storage'
import { route } from '@/routes'
 
export function useAuth() {
const [errors, setErrors] = useState({})
const [loading, setLoading] = useState(false)
const [accessToken, setAccessToken, removeAccessToken] = useLocalStorage('access_token', '')
 
const navigate = useNavigate()
 
const isLoggedIn = useMemo(() => !!accessToken, [accessToken])
 
useEffect(() => {
if (accessToken) {
axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`
}
}, [accessToken])
 
async function register(data) {
setErrors({})
setLoading(true)
 
return axios.post('auth/register', data)
.then((response) => {
setAccessToken(response.data.access_token)
navigate(route('vehicles.index'))
})
.catch(error => {
if (error.response.status === 422) {
setErrors(error.response.data.errors)
}
})
.finally(() => setLoading(false))
}
 
async function login(data) {
setErrors({})
setLoading(true)
 
return axios.post('/auth/login', data)
.then(response => {
setAccessToken(response.data.access_token)
navigate(route('parkings.active'))
})
.catch(error => {
if (error.response.status === 422) {
setErrors(error.response.data.errors)
}
})
.finally(() => setLoading(false))
}
 
async function logout(force = false) {
if (!force) {
await axios.post('auth/logout')
}
 
removeAccessToken()
navigate(route('login'))
}
 
return { register, login, errors, loading, isLoggedIn, logout }
}
  1. Update the src/views/auth/Login.jsx component with the following content.
import { useState } from 'react'
import { useAuth } from '@/hooks/useAuth'
import ValidationError from '@/components/ValidationError'
import IconSpinner from '@/components/IconSpinner'
 
function Login() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [remember, setRemember] = useState(false)
const { login, errors, loading } = useAuth()
 
async function handleSubmit(event) {
event.preventDefault()
 
await login({ email, password })
 
setPassword('')
}
 
return (
<form onSubmit={ handleSubmit } noValidate>
<div className="flex flex-col mx-auto md:w-96 w-full">
<h1 className="heading">Login</h1>
<div className="flex flex-col gap-2 mb-4">
<label htmlFor="email" className="required">Email</label>
<input
id="email"
name="email"
type="text"
value={ email }
onChange={ event => setEmail(event.target.value) }
className="form-input"
autoComplete="email"
disabled={ loading }
/>
<ValidationError errors={ errors } field="email" />
</div>
 
<div className="flex flex-col gap-2 mb-4">
<label htmlFor="password" className="required">Password</label>
<input
id="password"
name="password"
type="password"
value={ password }
onChange={ event => setPassword(event.target.value) }
className="form-input"
autoComplete="current-password"
disabled={ loading }
/>
<ValidationError errors={ errors } field="password" />
</div>
 
<div className="flex flex-col gap-2">
<label className="flex gap-2 items-center hover:cursor-pointer" htmlFor="remember">
<input
id="remember"
name="remember"
type="checkbox"
className="w-4 h-4"
checked={ remember }
onChange={ () => setRemember((previous) => !previous) }
disabled={ loading }
/>
<span className="select-none">Remember me</span>
</label>
</div>
 
<div className="border-t h-[1px] my-6"></div>
 
<div className="flex flex-col gap-2 mb-4">
<button type="submit" className="btn btn-primary" disabled={ loading }>
{ loading && <IconSpinner /> }
Login
</button>
</div>
</div>
</form>
)
}
 
export default Login

Here we have a new state variable for Remember me checkbox. It has a boolean value to determine checkbox is marked or not.

const [remember, setRemember] = useState(false)

The checkbox itself is defined like this:

<input
id="remember"
name="remember"
type="checkbox"
className="w-4 h-4"
checked={ remember }
onChange={ () => setRemember((previous) => !previous) }
disabled={ loading }
/>

This time we bind the value to the checked attribute and the checkbox will be marked as checked if the remember variable evaluates to true.

When updating the state value using the setRemember function, it can accept the setter function with a previous value as an argument, then we just invert the boolean value using !previous.

onChange={ () => setRemember((previous) => !previous) }

There is no need to redo any validation or loading state work, simply adjust the value of the field property in the ValidationError component to match the fields in the login form received from the API.

Previous: Authentication
avatar

Hi there! If you login and then go to "/login" on the address bar , the login form should not be there because you are already logged in. It should redirect to the vehicle or parking list. I'm not a great frontender, but I suppose this could be resolved within the routes file by checking if the user is authenticated and redirecting to /parkings or /vehicles, for example.

👍 1
avatar

Hey Cesar, Thanks for your comment. Regarding this issue, I've found an article with a video explaining exactly how to fix that here.

For our app, here is how I fixed it:

In routes/index.jsx:

<Route element={<AuthRoutes/>}>
    <Route path={route('vehicles.index')} element={<VehiclesList/>}/>
    <Route path={route('parkings.active')} element={<ActiveParkings/>}/>
</Route>

Then, create a component, in file components/AuthRoutes.jsx:

import useAuth from '@/hooks/useAuth.jsx'
import {Navigate, Outlet} from 'react-router-dom'
import {route} from '@/routes/index.jsx'

const AuthRoutes = () => {
  const {isLoggedIn} = useAuth()
  return (
    isLoggedIn ? <Outlet/> : <Navigate to={route('login')}/>
  )
}

export default AuthRoutes

Hope that helps!

avatar
You can use Markdown
avatar

The "remember" functionality is not being applied:

  // currently
  async function handleSubmit(event) {
    event.preventDefault()
 
    await login({ email, password })
 
    setPassword('')
  }
  // how the code should be
  async function handleSubmit(event) {
    event.preventDefault()
 
    await login({ email, password, remember_me: remember })
 
    setPassword('')
  }
avatar
You can use Markdown
avatar

An observation: since there's no UI change, data fetch or any other client-side operation that depends upon email, password or remember state variables, there's no reason for them to exist.

We could simply have static variables referencing the input DOM elements and getting the value to pass it to login from useAuth - the only place the input values are necessary:

    async function handleSubmit(event) {
        event.preventDefault()

        const email = document.querySelector('input[type="email"]').value
        const password = document.querySelector('input[type="password"]').value
        const remember = document.querySelector('input[type="checkbox"]').checked

        await login({ email, password, remember_me: remember })
    }
avatar
You can use Markdown
avatar
You can use Markdown