Auth Context and useAuth Hook
Authentication state needs to be accessible throughout the app — the navbar shows the username, protected routes redirect anonymous users, and forms include the user’s ID. React Context is the right tool.
AuthContext
Section titled “AuthContext”import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'import { TOKEN_KEY, USER_KEY } from '../constants'
interface AuthUser { userId: number username: string}
interface AuthContextValue { user: AuthUser | null token: string | null login: (token: string, user: AuthUser) => void logout: () => void isAuthenticated: boolean}
const AuthContext = createContext<AuthContextValue | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<AuthUser | null>(() => { const stored = localStorage.getItem(USER_KEY) return stored ? JSON.parse(stored) : null })
const [token, setToken] = useState<string | null>(() => localStorage.getItem(TOKEN_KEY) )
function login(newToken: string, newUser: AuthUser) { localStorage.setItem(TOKEN_KEY, newToken) localStorage.setItem(USER_KEY, JSON.stringify(newUser)) setToken(newToken) setUser(newUser) }
function logout() { localStorage.removeItem(TOKEN_KEY) localStorage.removeItem(USER_KEY) setToken(null) setUser(null) }
return ( <AuthContext.Provider value={{ user, token, login, logout, isAuthenticated: !!token && !!user, }}> {children} </AuthContext.Provider> )}
export function useAuth(): AuthContextValue { const context = useContext(AuthContext) if (!context) { throw new Error('useAuth must be used within an AuthProvider') } return context}Lazy initialization
Section titled “Lazy initialization”The useState initializers read from localStorage once on mount:
const [token, setToken] = useState<string | null>(() => localStorage.getItem(TOKEN_KEY))This is a lazy initializer — the function runs only once, not on every render. Without it, the initial state would be null and the user would see a brief “logged out” flash before the component reads localStorage.
Using useAuth anywhere
Section titled “Using useAuth anywhere”// In Navbarfunction Navbar() { const { user, isAuthenticated, logout } = useAuth()
return ( <nav> <Link to="/">Bulletin</Link> {isAuthenticated ? ( <> <span>Hello, {user!.username}</span> <button onClick={logout}>Logout</button> </> ) : ( <> <Link to="/login">Login</Link> <Link to="/register">Register</Link> </> )} </nav> )}Connecting to the axios interceptor
Section titled “Connecting to the axios interceptor”The auth interceptor reads from localStorage directly — it already works. But if you want to trigger logout via React state when a 401 occurs, inject the logout function into the interceptor:
// src/api/client.ts — update after AuthProvider is set upexport function setupAuthInterceptor(logout: () => void) { apiClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { logout() } return Promise.reject(error) } )}Call this inside AuthProvider or a component at the top of the tree.
Exercise
Section titled “Exercise”- Create
src/context/AuthContext.tsxwith the code above. - Wrap
Appwith<AuthProvider>. - Use
useAuthinNavbarto show the username when logged in. - Confirm that after calling
logout(), the user state clears and the navbar updates.
AuthContextstoresuser,token,login, andlogoutfor app-wide access.- Lazy
useStateinitializers read from localStorage once — no logged-out flash on reload. useAuththrows if used outsideAuthProvider— prevents silent bugs.isAuthenticatedis a derived boolean:!!token && !!user.