React - Login & Signup Form
Overview
Estimated time: 25–35 minutes
A practical login/signup form with client-side validation, show/hide password, and simulated async server responses.
Try it: Auth form
View source
function fakeApi(endpoint, body){
return new Promise((resolve, reject) => {
setTimeout(() => {
if (endpoint === 'signup'){
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(body.email)) return reject({message:'Invalid email'});
if (body.password.length < 8) return reject({message:'Password too short'});
if (body.email.endsWith('@taken.com')) return reject({message:'Email already registered'});
resolve({ok:true, user:{id:1, email:body.email}});
} else if (endpoint === 'login'){
if (body.email === '[email protected]' && body.password === 'password123') resolve({ok:true, user:{id:1, email:body.email}});
else reject({message:'Invalid credentials'});
}
}, 700);
});
}
function useForm(initial){
const [values, setValues] = React.useState(initial);
const [errors, setErrors] = React.useState({});
function validate(mode){
const e = {};
if (!values.email) e.email = 'Email required';
else if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(values.email)) e.email = 'Invalid email';
if (!values.password) e.password = 'Password required';
else if (mode==='signup' && values.password.length < 8) e.password = 'Use at least 8 characters';
if (mode==='signup' && values.password !== values.confirm) e.confirm = 'Passwords do not match';
setErrors(e); return e;
}
function onChange(e){ const {name, value, type, checked} = e.target; setValues(v => ({...v, [name]: type==='checkbox'? checked : value })); }
return {values, errors, validate, onChange, setValues};
}
function AuthForm(){
const [mode, setMode] = React.useState('login');
const {values, errors, validate, onChange, setValues} = useForm({email:'', password:'', confirm:'', remember:true});
const [showPwd, setShowPwd] = React.useState(false);
const [pending, setPending] = React.useState(false);
const [msg, setMsg] = React.useState('');
async function onSubmit(e){
e.preventDefault(); setMsg('');
const err = validate(mode); if (Object.keys(err).length) return;
setPending(true);
try{
const res = await fakeApi(mode, values);
setMsg(`Success! Welcome ${res.user.email}`);
if (mode==='signup') { setMode('login'); setValues(v => ({...v, password:'', confirm:''})); }
}catch(err){ setMsg(err.message || 'Request failed'); }
finally{ setPending(false); }
}
return (
<form onSubmit={onSubmit} style={{maxWidth:360}}>
<div style={{marginBottom:8}}>
<label>Email<br />
<input name="email" type="email" value={values.email} onChange={onChange} autoComplete="email" required />
</label>
<div style={{color:'crimson'}}>{errors.email}</div>
</div>
<div style={{marginBottom:8}}>
<label>Password<br />
<input name="password" type={showPwd?'text':'password'} value={values.password} onChange={onChange} autoComplete={mode==='login'? 'current-password' : 'new-password'} required />
</label>
<label style={{marginLeft:8}}><input type="checkbox" checked={showPwd} onChange={e => setShowPwd(e.target.checked)} /> Show</label>
<div style={{color:'crimson'}}>{errors.password}</div>
</div>
{mode==='signup' && (
<div style={{marginBottom:8}}>
<label>Confirm password<br />
<input name="confirm" type={showPwd?'text':'password'} value={values.confirm} onChange={onChange} required />
</label>
<div style={{color:'crimson'}}>{errors.confirm}</div>
</div>
)}
{mode==='login' && (
<label style={{display:'inline-flex', alignItems:'center', gap:6, marginBottom:8}}>
<input type="checkbox" name="remember" checked={values.remember} onChange={onChange} /> Remember me
</label>
)}
<div style={{display:'flex', gap:8, alignItems:'center'}}>
<button type="submit" disabled={pending}>{pending? 'Please wait…' : (mode==='login'? 'Log in' : 'Sign up')}</button>
<button type="button" onClick={()=> setMode(m => m==='login'?'signup':'login')} disabled={pending}>Switch to {mode==='login'? 'Sign up' : 'Log in'}</button>
</div>
<div aria-live="polite" style={{marginTop:8}}>{msg}</div>
</form>
);
}
ReactDOM.createRoot(document.getElementById('try-auth-form')).render(<AuthForm />);
Syntax primer
- Validate on submit; show field-level errors.
- Disable submit during async requests to prevent double submits.
Vocabulary
- Client-side validation: checks performed in the browser before sending data.
- Async flow: UI reflects loading and error states while waiting for server.
Common pitfalls
- Storing plaintext passwords—never log sensitive values.
- Not handling server errors—always show a helpful message.
Exercises
- Add password strength meter and requirements list.
- Implement email verification flow placeholder after signup success.
Checks for Understanding
- Why disable the submit button during async requests?
- How should you handle server-side validation errors in the UI?
Show answers
- To prevent duplicate submissions and inconsistent states.
- Show a clear message near the relevant field or at the form top; don’t expose sensitive details.