Heroes App implements a fully client-side authentication system built on React’sDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/ludwiigdev/Heroes_App/llms.txt
Use this file to discover all available pages before exploring further.
useReducer hook, the Context API, and localStorage. There is no backend or JWT involved — a user “logs in” by storing a simple user object in the browser, and every protected page checks that object before rendering. This page walks through every piece of the system, from the reducer and provider down to the route guards and post-login redirect logic.
How Authentication Works
The system has three moving parts that work together:State lives in useReducer
AuthProvider creates an authState object via useReducer. The reducer handles two action types — login and logout — and produces a new state each time one is dispatched.Persistence lives in localStorage
On login, the user object is written to
localStorage. On logout it is removed. A lazy initializer (init) reads from localStorage when the app first mounts, so a page refresh never logs the user out.Auth State Shape
Every component that reads fromAuthContext receives an object with this shape:
user is null after a logout action because the reducer returns { logged: false } with no user key, not because it is explicitly set to null. Consuming components should always guard with logged before accessing user.Action Types
Action type strings are centralised in a single constants file to avoid typo-prone hard-coded strings scattered across the codebase.The Reducer
authReducer is a pure function — given the same state and action it always produces the same next state.
| Action type | Payload | Next state |
|---|---|---|
[Auth] Login | { id, name } | { logged: true, user: { id, name } } |
[Auth] Logout | (none) | { logged: false } |
The AuthProvider
AuthProvider is the component that wires everything together. It owns the reducer, exposes helper functions, and renders the context provider around the rest of the application.
The init lazy initializer
useReducer accepts a third argument — an initializer function. init runs exactly once when the provider mounts and builds the initial state from whatever is already in localStorage:
localStorage contains a serialised user object, !!user evaluates to true and the app boots in a logged-in state. If the key is absent, JSON.parse returns null, so logged becomes false.
login(name)
Step-by-step: what login() does
Step-by-step: what login() does
- Constructs a user object:
{ id: "123", name }. - Serialises it to
localStorageunder the key"user". - Dispatches a
types.loginaction with the user as payload. - The reducer spreads the new user into state and sets
logged: true.
logout()
Step-by-step: what logout() does
Step-by-step: what logout() does
- Removes the
"user"key fromlocalStorage. - Dispatches a
types.logoutaction with no payload. - The reducer replaces the entire state with
{ logged: false }.
Context value
The value exposed to consumers is a flat object that merges state fields with the two action helpers:Route Protection
Two thin wrapper components gate access to routes based onlogged.
PrivateRoute
Renders itschildren only when the user is logged in. If not, it saves the current path to localStorage and redirects to /login.
PrivateRoute writes lastPath on every render, not only when redirecting. This means it always tracks the most recent private URL the user attempted to visit, even if they were already logged in when they navigated there.PublicRoute
Renders itschildren only when the user is not logged in. An already-authenticated user who somehow lands on /login is immediately sent to /marvel.
The lastPath Redirect Flow
One of the most important UX details is that after logging in, the user lands on the page they were trying to reach — not just the app root.
User visits a protected route while logged out
PrivateRoute fires. It reads pathname + search from useLocation(), writes it to localStorage as "lastPath", and redirects to /login.User clicks Login on LoginPage
LoginPage reads localStorage.getItem("lastPath"), falls back to "/" if missing, calls login(), then navigates to that path with replace: true.Using AuthContext in a Component
Any component insideAuthProvider can subscribe to auth state by calling useContext(AuthContext).
