JWT Auth in React: From HTTP Statelessness to Zustand, Axios, and Route Guards
JWT remains the default stateless auth mechanism for SPAs and mobile backends, but many frontend developers treat it as a black box. Understanding the signing and verification steps, the real XSS tradeoffs of localStorage, and how Zustand and Axios interceptors compose into a clean auth layer prevents the silent security and UX bugs that ship to production.
JWT authentication in a React app touches every layer: the stateless HTTP protocol, token structure and signing, client-side state persistence, and request plumbing. This walkthrough traces the full lifecycle of a login request, from form validation through token issuance, storage in Zustand and localStorage, automatic attachment via Axios interceptors, and route protection with React Router v6. It also unpacks the internals of Zustand's publish-subscribe model and explains why selectors prevent unnecessary re-renders.
Mock service workers simulate backend delays and JWT verification without a real server, making the frontend development loop realistic. The piece also addresses common security questions, including why JWTs are not encrypted, the XSS risks of localStorage, and why the signing secret must never appear in frontend code.
Practical details like the `Bearer` prefix, the `replace` prop on navigation to avoid redirect loops, and using `useAuthStore.getState()` inside non-React contexts round out a guide that connects protocol-level concepts to everyday React engineering.
Many developers mistake JWT for encryption; the article correctly stresses that its real job is tamper detection, which has direct consequences for what you put in the payload.
Coupling Zustand with localStorage is a pragmatic but leaky abstraction — cross-tab logout requires an explicit `storage` event listener that most tutorials omit.
The `Bearer` prefix is a small detail that, when missing, forces backend teams to write custom parsing logic instead of using standard OAuth2 libraries.
Using three short-circuit expressions instead of an if-else block in JSX is a deliberate DOM manipulation choice, not just a style preference, because it avoids residual event listeners on hidden elements.
React 18's automatic batching means the order of `setAuth` and `navigate` is less fragile than it appears, but the mental model of 'update state then route' still matters for older React versions.