Professional React WorkflowPro· 45 min read

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: who is logged in, plus login/logout
// 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:

  1. The user state starts by checking localStorage for a saved token. If one exists (from a previous session), the user is restored as already logged in; otherwise it is null. This is what keeps you logged in across reloads.
  2. login(token) saves the token to localStorage and sets the user in state. logout() removes the token and clears the user.
  3. We share { user, login, logout } through AuthContext.Provider, and export a tidy useAuth() hook so any component can read or change the auth state with one line.

2) A login page that calls login()

On successful login, store the token and navigate
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>.

RequireAuth guards any protected route
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?

Answer: If there is no authenticated user, the guard returns <Navigate to="/login" />, redirecting away from the protected page; otherwise it renders the page.

✍️ Practice

  1. Build an AuthContext with login/logout that persists a token in localStorage.
  2. Wrap a /dashboard route in a RequireAuth guard and confirm logged-out users are redirected to /login.

🏠 Homework

  1. Add a navbar that shows a Log out button when logged in and a Log in link when not, reading from useAuth().
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →