Skip to content

Logout and Token Expiry

Logout clears the token and user state. Token expiry needs to be handled automatically — when the API returns 401, the user should be logged out and redirected gracefully.

The logout function in AuthContext handles everything:

function logout() {
localStorage.removeItem(TOKEN_KEY)
localStorage.removeItem(USER_KEY)
setToken(null)
setUser(null)
}

Calling logout() from any component triggers a re-render of the entire tree — isAuthenticated becomes false, the navbar updates, and protected routes redirect.

// In Navbar.tsx
const { user, logout } = useAuth()
function handleLogout() {
logout()
navigate('/')
}
<button onClick={handleLogout}>Logout</button>

When the JWT expires (after 24 hours), the API returns 401 Unauthorized. The axios response interceptor can handle this globally:

src/api/client.ts
let logoutCallback: (() => void) | null = null
export function setLogoutCallback(fn: () => void) {
logoutCallback = fn
}
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401 && logoutCallback) {
logoutCallback()
}
return Promise.reject(error)
}
)

Register the callback in AuthProvider:

src/context/AuthContext.tsx
import { setLogoutCallback } from '../api/client'
export function AuthProvider({ children }: { children: ReactNode }) {
// ...
function logout() {
localStorage.removeItem(TOKEN_KEY)
localStorage.removeItem(USER_KEY)
setToken(null)
setUser(null)
}
// Register logout with the axios interceptor
useEffect(() => {
setLogoutCallback(logout)
return () => setLogoutCallback(null as any)
}, [])
// ...
}

Now any 401 response automatically calls logout() — the user is logged out and the navbar updates without any per-component handling.

Optionally, show a message when auto-logged out:

function logout(reason?: 'expired') {
localStorage.removeItem(TOKEN_KEY)
localStorage.removeItem(USER_KEY)
setToken(null)
setUser(null)
if (reason === 'expired') {
// Navigate to login with a message
window.location.href = '/login?expired=true'
}
}
// In LoginPage.tsx
const expired = new URLSearchParams(window.location.search).get('expired')
{expired && <p className="notice">Your session expired. Please log in again.</p>}

Token refresh (advanced — not in Bulletin)

Section titled “Token refresh (advanced — not in Bulletin)”

Production apps often use refresh tokens:

  1. Short-lived access token (15 minutes)
  2. Long-lived refresh token (7 days)
  3. When the access token expires, automatically exchange the refresh token for a new one

This is more complex but eliminates the “session expired” UX problem. Bulletin uses a simple 24-hour token — acceptable for a learning app.

  1. Add a Logout button to the Navbar that calls handleLogout.
  2. Set up the 401 interceptor with the callback pattern.
  3. Test: register, wait for the token to expire (change expiresIn: '5s' in the API temporarily), make a request, confirm logout happens.
  • logout() clears localStorage and React state — triggers re-render of the entire tree.
  • The axios 401 interceptor handles token expiry globally — no per-component handling needed.
  • Register the logout callback from AuthProvider using a useEffect.
  • Token refresh is the production solution to session expiry — acceptable to skip for learning.