How to build a signup page in React
Building a robust signup page is essential for user onboarding in any application, requiring proper form handling, validation, and secure password management. With over 10 years of experience building React applications since 2014 and as the creator of CoreUI, a widely used open-source UI library, I’ve implemented countless registration systems in production environments. The most effective approach is to use controlled components with React hooks for form state management, combined with real-time validation and password strength indicators. This method provides immediate feedback to users while ensuring data integrity before submission.
Use React hooks with controlled form inputs, real-time validation, and password strength checking to build a secure and user-friendly signup page.
import { useState } from 'react'
function SignupPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
terms: false
})
const handleSubmit = async (e) => {
e.preventDefault()
console.log('Form submitted:', formData)
}
const handleChange = (e) => {
const { name, value, type, checked } = e.target
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}))
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={formData.name} onChange={handleChange} />
<input name="email" value={formData.email} onChange={handleChange} />
<input name="password" type="password" value={formData.password} onChange={handleChange} />
<button type="submit">Sign Up</button>
</form>
)
}
This basic signup form uses the useState hook to manage all form fields in a single state object. The handleChange function updates the appropriate field when users type, and handleSubmit prevents the default form submission to handle registration logic with JavaScript. All inputs are controlled components, meaning React manages their values, providing full control over the form state.
Adding Form Validation
import { useState } from 'react'
function SignupPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
terms: false
})
const [errors, setErrors] = useState({})
const validate = () => {
const newErrors = {}
if (!formData.name.trim()) {
newErrors.name = 'Name is required'
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required'
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Invalid email format'
}
if (!formData.password) {
newErrors.password = 'Password is required'
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters'
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match'
}
if (!formData.terms) {
newErrors.terms = 'You must accept the terms and conditions'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = async (e) => {
e.preventDefault()
if (validate()) {
console.log('Form is valid, submitting...')
}
}
const handleChange = (e) => {
const { name, value, type, checked } = e.target
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}))
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }))
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<input name="name" value={formData.name} onChange={handleChange} placeholder="Full Name" />
{errors.name && <span style={{ color: 'red' }}>{errors.name}</span>}
</div>
<div>
<input name="email" value={formData.email} onChange={handleChange} placeholder="Email" />
{errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
</div>
<div>
<input name="password" type="password" value={formData.password} onChange={handleChange} placeholder="Password" />
{errors.password && <span style={{ color: 'red' }}>{errors.password}</span>}
</div>
<div>
<input name="confirmPassword" type="password" value={formData.confirmPassword} onChange={handleChange} placeholder="Confirm Password" />
{errors.confirmPassword && <span style={{ color: 'red' }}>{errors.confirmPassword}</span>}
</div>
<div>
<label>
<input name="terms" type="checkbox" checked={formData.terms} onChange={handleChange} />
I accept the terms and conditions
</label>
{errors.terms && <span style={{ color: 'red' }}>{errors.terms}</span>}
</div>
<button type="submit">Sign Up</button>
</form>
)
}
The validation logic checks each field for common issues like empty values, invalid email format, weak passwords, and mismatched password confirmation. The validate() function returns true if all fields are valid, allowing form submission to proceed. When users start typing in a field with an error, the error message clears immediately, providing a responsive user experience. Error messages display directly below each input field for clarity.
Implementing Password Strength Indicator
import { useState } from 'react'
function SignupPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
terms: false
})
const [passwordStrength, setPasswordStrength] = useState(0)
const calculatePasswordStrength = (password) => {
let strength = 0
if (password.length >= 8) strength++
if (password.length >= 12) strength++
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++
if (/\d/.test(password)) strength++
if (/[^a-zA-Z0-9]/.test(password)) strength++
return strength
}
const getStrengthLabel = (strength) => {
if (strength === 0) return 'Very Weak'
if (strength === 1) return 'Weak'
if (strength === 2) return 'Fair'
if (strength === 3) return 'Good'
if (strength === 4) return 'Strong'
return 'Very Strong'
}
const getStrengthColor = (strength) => {
if (strength <= 1) return '#ff4444'
if (strength === 2) return '#ffaa00'
if (strength === 3) return '#88cc00'
return '#00cc44'
}
const handleChange = (e) => {
const { name, value, type, checked } = e.target
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}))
if (name === 'password') {
setPasswordStrength(calculatePasswordStrength(value))
}
}
return (
<form>
<div>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
{formData.password && (
<div style={{ marginTop: '5px' }}>
<div style={{
height: '5px',
background: '#e0e0e0',
borderRadius: '3px',
overflow: 'hidden'
}}>
<div style={{
width: `${passwordStrength * 20}%`,
height: '100%',
background: getStrengthColor(passwordStrength),
transition: 'all 0.3s'
}} />
</div>
<span style={{
fontSize: '12px',
color: getStrengthColor(passwordStrength),
marginTop: '2px',
display: 'block'
}}>
Password Strength: {getStrengthLabel(passwordStrength)}
</span>
</div>
)}
</div>
</form>
)
}
The password strength indicator evaluates passwords based on length, use of uppercase and lowercase letters, numbers, and special characters. Each criterion met increases the strength score, which drives a visual progress bar and color-coded label. The bar fills from left to right and changes color from red to green as the password becomes stronger, providing immediate visual feedback. This encourages users to create secure passwords during registration.
Integrating API Submission
import { useState } from 'react'
function SignupPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
terms: false
})
const [loading, setLoading] = useState(false)
const [submitError, setSubmitError] = useState('')
const [success, setSuccess] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
setSubmitError('')
try {
const response = await fetch('https://api.example.com/auth/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: formData.name,
email: formData.email,
password: formData.password
})
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.message || 'Signup failed')
}
const data = await response.json()
setSuccess(true)
console.log('Signup successful:', data)
// Redirect to login or dashboard
} catch (error) {
setSubmitError(error.message)
} finally {
setLoading(false)
}
}
if (success) {
return (
<div>
<h2>Signup Successful!</h2>
<p>Please check your email to verify your account.</p>
</div>
)
}
return (
<form onSubmit={handleSubmit}>
{submitError && (
<div style={{
padding: '10px',
background: '#ffebee',
color: '#c62828',
borderRadius: '4px',
marginBottom: '15px'
}}>
{submitError}
</div>
)}
<button type="submit" disabled={loading}>
{loading ? 'Signing Up...' : 'Sign Up'}
</button>
</form>
)
}
API integration sends user data to the backend for account creation. The component tracks loading state to disable the submit button and show loading feedback during the request. If signup succeeds, a success message displays instead of the form. If it fails, the error message appears at the top of the form, providing clear feedback. The try-catch-finally pattern ensures loading state is always reset, even if an error occurs.
Best Practice Note
For production applications, consider using pre-built form components and authentication flows from libraries like CoreUI for React, which provide accessible, tested components with built-in validation and styling. You can explore the CoreUI React Forms documentation for production-ready form components. For related authentication flows, check out how to build a login page in React and how to build a dashboard in React. This approach is the same strategy we use in CoreUI to ensure consistent, accessible user experiences across authentication flows.



