How to build a login page in React
Building a secure and user-friendly login page is the foundation of most web applications, requiring careful attention to form validation, error handling, and API integration. With 10 years of experience in React development since 2014 and as the creator of CoreUI, I’ve built authentication systems for countless enterprise applications and admin dashboards. From my expertise, the most effective approach is to create a controlled form component with proper validation, loading states, and error feedback that integrates seamlessly with your backend API. This method provides immediate user feedback, prevents invalid submissions, and handles authentication errors gracefully.
Build a controlled form component with email and password inputs, validation, and API integration.
import { useState } from 'react'
const LoginPage = () => {
const [formData, setFormData] = useState({
email: '',
password: ''
})
const [errors, setErrors] = useState({})
const [loading, setLoading] = useState(false)
const handleChange = (e) => {
const { name, value } = e.target
setFormData(prev => ({
...prev,
[name]: value
}))
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }))
}
}
const handleSubmit = async (e) => {
e.preventDefault()
const newErrors = validateForm(formData)
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
setLoading(true)
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
if (!response.ok) {
throw new Error('Login failed')
}
const data = await response.json()
localStorage.setItem('token', data.token)
window.location.href = '/dashboard'
} catch (error) {
setErrors({ submit: 'Invalid email or password' })
} finally {
setLoading(false)
}
}
return (
<div className="login-container">
<form onSubmit={handleSubmit} className="login-form">
<h1>Login</h1>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
disabled={loading}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
disabled={loading}
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
{errors.submit && <div className="error-message">{errors.submit}</div>}
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
</div>
)
}
const validateForm = (data) => {
const errors = {}
if (!data.email) {
errors.email = 'Email is required'
} else if (!/\S+@\S+\.\S+/.test(data.email)) {
errors.email = 'Email is invalid'
}
if (!data.password) {
errors.password = 'Password is required'
} else if (data.password.length < 6) {
errors.password = 'Password must be at least 6 characters'
}
return errors
}
export default LoginPage
How It Works
This login page uses controlled components where form inputs are bound to React state through the value prop and updated via onChange handlers. The handleSubmit function prevents the default form submission, validates the input data, and makes an API call to authenticate the user. On successful login, the authentication token is stored in localStorage and the user is redirected to the dashboard. Error states provide feedback for validation failures and API errors.
Adding Loading State
Enhance the user experience with loading indicators:
const LoginPage = () => {
const [loading, setLoading] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || 'Login failed')
}
localStorage.setItem('token', data.token)
window.location.href = '/dashboard'
} catch (error) {
setErrors({ submit: error.message })
} finally {
setLoading(false)
}
}
return (
<button type="submit" disabled={loading}>
{loading ? (
<>
<span className="spinner" />
Logging in...
</>
) : (
'Login'
)}
</button>
)
}
The loading state disables form inputs and the submit button during API calls, preventing duplicate submissions. The button text changes to indicate the login is in progress.
Real-time Validation
Add real-time validation as users type:
const LoginPage = () => {
const [touched, setTouched] = useState({})
const handleBlur = (e) => {
const { name } = e.target
setTouched(prev => ({ ...prev, [name]: true }))
const fieldErrors = validateField(name, formData[name])
if (fieldErrors) {
setErrors(prev => ({ ...prev, [name]: fieldErrors }))
} else {
setErrors(prev => {
const newErrors = { ...prev }
delete newErrors[name]
return newErrors
})
}
}
const validateField = (name, value) => {
switch (name) {
case 'email':
if (!value) return 'Email is required'
if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid'
return null
case 'password':
if (!value) return 'Password is required'
if (value.length < 6) return 'Password must be at least 6 characters'
return null
default:
return null
}
}
return (
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
/>
)
}
The onBlur handler validates each field when the user leaves it, providing immediate feedback. The touched state tracks which fields have been interacted with to avoid showing errors prematurely.
Remember Me Functionality
Add a “remember me” checkbox to persist user sessions:
const LoginPage = () => {
const [rememberMe, setRememberMe] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
const data = await response.json()
const storage = rememberMe ? localStorage : sessionStorage
storage.setItem('token', data.token)
window.location.href = '/dashboard'
} catch (error) {
setErrors({ submit: 'Invalid email or password' })
}
}
return (
<div className="form-check">
<input
type="checkbox"
id="rememberMe"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
/>
<label htmlFor="rememberMe">Remember me</label>
</div>
)
}
When “remember me” is checked, the token is stored in localStorage for persistence across browser sessions. Otherwise, it’s stored in sessionStorage and cleared when the browser closes.
Password Visibility Toggle
Allow users to toggle password visibility:
const LoginPage = () => {
const [showPassword, setShowPassword] = useState(false)
return (
<div className="form-group password-field">
<label htmlFor="password">Password</label>
<div className="input-wrapper">
<input
type={showPassword ? 'text' : 'password'}
id="password"
name="password"
value={formData.password}
onChange={handleChange}
/>
<button
type="button"
className="toggle-password"
onClick={() => setShowPassword(!showPassword)}
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? '👁️' : '👁️🗨️'}
</button>
</div>
</div>
)
}
The toggle button switches the input type between ‘password’ and ’text’, allowing users to verify they’ve typed their password correctly.
Forgot Password Link
Add a forgot password link with navigation:
import { Link } from 'react-router-dom'
const LoginPage = () => {
return (
<form onSubmit={handleSubmit}>
<div className="form-footer">
<Link to="/forgot-password">Forgot password?</Link>
</div>
<button type="submit">Login</button>
<div className="signup-link">
Don't have an account? <Link to="/signup">Sign up</Link>
</div>
</form>
)
}
These links provide navigation to password recovery and account creation pages, improving the overall authentication flow.
Best Practice Note
At CoreUI, our React admin templates include production-ready login pages with advanced features like OAuth integration, two-factor authentication, and role-based redirects. When building authentication flows, always validate on both the client and server side - never trust client-side validation alone. For complete authentication systems, consider integrating your login page with a dashboard in React and complementing it with a signup page in React. Store authentication tokens securely, use HTTPS in production, and implement proper CSRF protection. Remember to handle edge cases like expired sessions, account lockouts after failed attempts, and email verification requirements for a robust authentication experience.



