Register and Login Forms
The register and login pages are the entry point to Bulletin. This lesson builds both forms — controlled inputs, API submission, error handling, and calling login() from Auth Context.
Login form
Section titled “Login form”import { useState, type FormEvent } from 'react'import { useNavigate, Link } from 'react-router-dom'import { useAuth } from '../context/AuthContext'import { authApi } from '../api/auth'import axios from 'axios'
export default function LoginPage() { const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState<string | null>(null) const [submitting, setSubmitting] = useState(false)
const { login } = useAuth() const navigate = useNavigate()
async function handleSubmit(e: FormEvent) { e.preventDefault() if (!username.trim() || !password) return
setSubmitting(true) setError(null)
try { const { data } = await authApi.login({ username: username.trim(), password }) login(data.token, { userId: data.userId, username: data.username }) navigate('/') } catch (err) { setError( axios.isAxiosError(err) ? err.response?.data?.error || 'Login failed' : 'Unexpected error' ) } finally { setSubmitting(false) } }
return ( <div className="auth-page"> <h1>Log in to Bulletin</h1> <form onSubmit={handleSubmit}> {error && <p className="form-error">{error}</p>} <label> Username <input type="text" value={username} onChange={e => setUsername(e.target.value)} required autoFocus /> </label> <label> Password <input type="password" value={password} onChange={e => setPassword(e.target.value)} required /> </label> <button type="submit" disabled={submitting}> {submitting ? 'Logging in...' : 'Log in'} </button> </form> <p>Don't have an account? <Link to="/register">Register</Link></p> </div> )}Register form
Section titled “Register form”import { useState, type FormEvent } from 'react'import { useNavigate, Link } from 'react-router-dom'import { useAuth } from '../context/AuthContext'import { authApi } from '../api/auth'import axios from 'axios'
export default function RegisterPage() { const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState<string | null>(null) const [submitting, setSubmitting] = useState(false)
const { login } = useAuth() const navigate = useNavigate()
async function handleSubmit(e: FormEvent) { e.preventDefault() setSubmitting(true) setError(null)
try { const { data } = await authApi.register({ username: username.trim(), password }) login(data.token, { userId: data.userId, username: data.username }) navigate('/') } catch (err) { setError( axios.isAxiosError(err) ? err.response?.data?.error || 'Registration failed' : 'Unexpected error' ) } finally { setSubmitting(false) } }
return ( <div className="auth-page"> <h1>Create your Bulletin account</h1> <form onSubmit={handleSubmit}> {error && <p className="form-error">{error}</p>} <label> Username <input type="text" value={username} onChange={e => setUsername(e.target.value)} placeholder="3–30 chars, letters/numbers/_" required /> </label> <label> Password <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="At least 8 characters" required minLength={8} /> </label> <button type="submit" disabled={submitting}> {submitting ? 'Creating account...' : 'Create account'} </button> </form> <p>Already have an account? <Link to="/login">Log in</Link></p> </div> )}What happens after login
Section titled “What happens after login”authApi.login()callsPOST /auth/loginon the API- The API returns
{ token, userId, username } login(token, { userId, username })stores in localStorage and React statenavigate('/')takes the user to the feed- The axios interceptor now attaches
Authorization: Bearer <token>to all requests
Exercise
Section titled “Exercise”- Build both pages.
- Test the full flow: register a new user, confirm redirect to feed.
- Log out, then log back in.
- Try registering with a duplicate username — confirm the error message appears.
- Controlled inputs +
handleSubmitis the standard React form pattern. - After successful auth, call
login(token, user)fromuseAuth()thennavigate('/'). - Display API error messages directly — the API returns useful human-readable errors.
- Disable the submit button while
submitting— prevents double-submission.