跪拜 Guibai
← Back to the summary

The Missing Piece in JWT Auth: Route Guards That Remember Where You Were Going

Now there's one last problem:

Even though we already have:

token
user

A user can still manually type:

/pay

and directly access the Pay page.

So we still need to solve:

Unauthenticated users cannot enter protected pages.

This is:

Route Guard.


1. Why do we need a route guard?

Assume:

/pay

is a page that only logged-in users can access.

Then we want:

Logged in   → can access /pay
Not logged in → redirect to /login

This check does not belong to the Pay page's own business logic.

So we create a dedicated component:

src/components/RequireAuth.jsx

Its job is simple:

Protect the page.


2. Creating RequireAuth

Code:

import { Navigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../store/user';

function RequireAuth({ children }) {
  const token = useAuthStore(state => state.token);
  const location = useLocation();

  if (!token) {
    return (
      <Navigate
        to="/login"
        state={{ from: location.pathname }}
        replace
      />
    );
  }

  return children;
}

export default RequireAuth;

The core check here:

if (!token)

Means:

No login credential.

What to do then?

<Navigate to="/login" />

Go straight to the login page.


3. Why wrap Pay inside RequireAuth?

In the route:

<Route
  path="/pay"
  element={
    <RequireAuth>
      <Pay />
    </RequireAuth>
  }
/>

This can be understood as:

Access /pay
        ↓
First go through RequireAuth
        ↓
Check token
        ↓
Has token → Pay
No token → Login

So RequireAuth is like a door.

The Pay page is the content behind the door.


4. Why not just write the check directly in Pay.jsx?

Of course you could.

But if later:

Pay
Order
Profile
Admin
Settings

all require login.

Would you write in every single page:

if (!token) {
  navigate('/login');
}

That would repeat a lot of code.

So we extract this common logic into:

RequireAuth

Now all pages that need protection can reuse it.


5. Why return to the original page after login?

Suppose a user originally wanted to visit:

/pay

but was not logged in.

So:

/pay
 ↓
/login

If after a successful login they always jump to:

/

the user would feel:

"I clearly wanted to go to Pay just now, why did I end up on the homepage after logging in?"

So RequireAuth records:

state={{ from: location.pathname }}

Meaning:

Tell the login page where I came from before.


6. Login page reads from

Login page:

const location = useLocation();

const from = location.state?.from || '/';

If the user previously visited:

/pay

then:

from === '/pay'

If no source was recorded:

from === '/'

7. Redirect back after successful login

Login success:

const res = await login(formData);

Then:

if (res.code === 0) {
  setAuth({
    token: res.token,
    user: res.user
  });

  navigate(from, {
    replace: true
  });
}

Thus:

User visits /pay
        ↓
RequireAuth checks token
        ↓
No token
        ↓
Redirect to /login
        ↓
Save from = /pay
        ↓
User completes login
        ↓
Server returns Token
        ↓
setAuth()
        ↓
navigate('/pay')
        ↓
Enter Pay

This forms the complete login redirect experience.


8. Now we can finally string the whole project together

At this point, all the main parts of this demo have appeared:

User visits a protected page
        ↓
RequireAuth checks token in Zustand
        ↓
No token → redirect to login page
        ↓
User fills in username and password
        ↓
Login calls user.js login()
        ↓
Axios sends /api/login
        ↓
Mock backend verifies username and password
        ↓
jwt.sign() generates Token
        ↓
Token returned to frontend
        ↓
Zustand saves token and user
        ↓
Also saved to localStorage
        ↓
Subsequent Axios requests automatically add Authorization
        ↓
Mock backend reads Bearer Token
        ↓
jwt.verify() verifies Token
        ↓
Verification success → return protected data

This chain is the core of the entire demo.


9. Finally, distinguish a few very easily confused things

Many concepts appeared in this demo, and it's easy to mix them all up when first learning.

API Layer

For example:

src/api/user.js
src/api/repo.js

Solves:

How the frontend organizes interface requests.


Mock

For example:

mock/user.js

Solves:

When there is no real backend during development, who simulates the backend interfaces.


JWT

For example:

jwt.sign()
jwt.verify()

Solves:

How the server issues an identity credential to the user, and how to verify that credential.


Axios Interceptor

For example:

instance.interceptors.request.use(...)

Solves:

How to make every request automatically carry the Token, instead of writing it interface by interface.


Zustand

For example:

useAuthStore(...)

Solves:

How to share the current login state inside a React application.


localStorage

Solves:

After refreshing the page, the Token can still be retrieved.


RequireAuth

Solves:

Whether a user who is not logged in can enter a certain page.


Finally: What you should really remember is not the five articles

If you only memorize API names from this demo, it will quickly become a mess.

What you should really remember are the following "whys":

Why do we need an API layer?

Because you can't let every React page manage request details on its own.

Why do we need Mock?

Because during frontend development, you need an environment that can simulate the backend.

Why do we need a Token?

Because HTTP is stateless, the server needs a credential to know "who you are."

Why do we need an Axios interceptor?

Because every request needs a Token, and common logic should be handled uniformly.

Why do we need Zustand?

Because multiple React components need to know who the current user is; login state is global state.

Why do we need localStorage?

Because React's in-memory state is lost after a refresh; you need a persistent place to save the Token.

Why do we need RequireAuth?

Because "having a Token or not" not only affects requests, but also affects whether a user can enter a certain page.

Ultimately, you should be able to say this one sentence yourself:

After a user logs in successfully, the server uses JWT to issue a Token; the frontend saves the Token and puts it into the login state Store; afterward, the Axios interceptor automatically puts the Token into the Authorization Header before the request is sent; after the server receives the request, it verifies the identity through jwt.verify(); and React Router then uses the global login state to decide whether the user can access protected pages.

If you can truly explain this sentence clearly, you have basically digested the main thread of this demo.