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.