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

  1. Add password strength meter and requirements list.
  2. Implement email verification flow placeholder after signup success.

Checks for Understanding

  1. Why disable the submit button during async requests?
  2. How should you handle server-side validation errors in the UI?
Show answers
  1. To prevent duplicate submissions and inconsistent states.
  2. Show a clear message near the relevant field or at the form top; don’t expose sensitive details.