Authentication & Protected Routes
Log a user in, remember them, and guard pages so only signed-in users can reach them.
What you will learn
- Store who is logged in with context
- Persist a token across page reloads
- Build a route guard that redirects logged-out users
What “auth” means in a React app
Authentication (auth for short) is proving who the user is. The usual flow: the user enters email and password, your back-end checks them and sends back a token — a long secret string that proves “this person is logged in” (commonly a JWT, a JSON Web Token). The React app stores that token and sends it with future requests. Protected routes are pages only logged-in users may see; a logged-out visitor gets redirected to the login page.
React itself does not log anyone in — your server does that. React’s job is to (1) remember the logged-in user, (2) keep the token across page reloads, and (3) guard certain routes. Let us build those three pieces. This is real project code, shown with Output.
1) Remember the user with context
We hold the current user (or null if logged out) in a context so any component can read it, and we save the token in localStorage so a page refresh does not log the user out.
// AuthContext.jsx
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(() => {
const saved = localStorage.getItem('token');
return saved ? { token: saved } : null; // restore on reload
});
function login(token) {
localStorage.setItem('token', token); // persist
setUser({ token });
}
function logout() {
localStorage.removeItem('token');
setUser(null);
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);Reading it through:
- The
userstate starts by checkinglocalStoragefor a savedtoken. If one exists (from a previous session), the user is restored as already logged in; otherwise it isnull. This is what keeps you logged in across reloads. login(token)saves the token tolocalStorageand sets the user in state.logout()removes the token and clears the user.- We share
{ user, login, logout }throughAuthContext.Provider, and export a tidyuseAuth()hook so any component can read or change the auth state with one line.
2) A login page that calls login()
import { useAuth } from './AuthContext.jsx';
import { useNavigate } from 'react-router-dom';
function Login() {
const { login } = useAuth();
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
// ask your server to verify the credentials:
// const res = await fetch('/api/login', { ... });
// const { token } = await res.json();
const token = 'fake-jwt-token'; // pretend the server returned this
login(token);
navigate('/dashboard'); // send them to a protected page
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Log in</button>
</form>
);
}On submit, the real version would fetch your /api/login endpoint, which returns a token. We then call login(token) (which saves it everywhere) and use React Router’s useNavigate to send the user to a protected page like /dashboard.
3) The route guard
A route guard is a small wrapper component: if there is a logged-in user it shows the page; if not, it redirects to the login screen using React Router’s <Navigate>.
import { Navigate } from 'react-router-dom';
import { useAuth } from './AuthContext.jsx';
function RequireAuth({ children }) {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" replace />; // not logged in → bounce to login
}
return children; // logged in → show the page
}
// In your routes:
// <Route path="/dashboard" element={
// <RequireAuth><Dashboard /></RequireAuth>
// } />The guard reads user from useAuth(). If user is null (logged out), it returns <Navigate to="/login" replace />, which immediately redirects to the login page (the replace stops the protected URL from cluttering the back button). If a user is logged in, it simply returns children — the actual page. You wrap any route you want to protect in <RequireAuth>...</RequireAuth>.
Note: Output:
Log in, and /dashboard shows normally. Click Log out (which clears the token), then try to open /dashboard again — you are instantly redirected to /login. Refresh the page while logged in and you stay logged in, because the token was restored from localStorage.
Tip: To send the token on API calls, add it to the request headers: fetch(url, { headers: { Authorization: 'Bearer ' + token } }). Your server reads that header to know who is asking.
Watch out: Storing tokens in localStorage is fine for learning, but it is readable by any script on the page — a risk if your site is vulnerable to script injection (XSS). Production apps often use secure, httpOnly cookies set by the server instead. Know the trade-off.
Q. What does a protected-route guard like RequireAuth do when there is no logged-in user?
✍️ Practice
- Build an
AuthContextwithlogin/logoutthat persists a token inlocalStorage. - Wrap a
/dashboardroute in aRequireAuthguard and confirm logged-out users are redirected to/login.
🏠 Homework
- Add a navbar that shows a Log out button when logged in and a Log in link when not, reading from
useAuth().