Skip to content

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.

src/pages/LoginPage.tsx
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>
)
}
src/pages/RegisterPage.tsx
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>
)
}
  1. authApi.login() calls POST /auth/login on the API
  2. The API returns { token, userId, username }
  3. login(token, { userId, username }) stores in localStorage and React state
  4. navigate('/') takes the user to the feed
  5. The axios interceptor now attaches Authorization: Bearer <token> to all requests
  1. Build both pages.
  2. Test the full flow: register a new user, confirm redirect to feed.
  3. Log out, then log back in.
  4. Try registering with a duplicate username — confirm the error message appears.
  • Controlled inputs + handleSubmit is the standard React form pattern.
  • After successful auth, call login(token, user) from useAuth() then navigate('/').
  • Display API error messages directly — the API returns useful human-readable errors.
  • Disable the submit button while submitting — prevents double-submission.