How React Router Guards, Redirects, and Remembers Where Users Were Going
Getting the history stack wrong during auth redirects creates infinite back-button loops that trap users. The `replace` + `location.state` pattern shown here is the standard fix, and the same approach works for any conditional navigation — expired sessions, multi-step wizards, or post-payment flows.
Route redirection in React Router is a full design pattern, not a simple jump. Three real-world scenarios — campaign teardowns, path normalization, and conditional auth redirects — each demand careful use of `replace` to avoid trapping users in history-stack loops. The `Navigate` component carries state through `location.state`, an invisible channel that survives client-side navigation but never touches the server.
The ProtectRoute component acts as a gate: it reads `localStorage` for a login flag, and when that flag is absent, it redirects to `/login` while embedding the current `location` object into the navigation state. This single object preserves the full pathname, query string, and hash, so a user arriving at `/pay?coupon=SUMMER50` returns to exactly that URL after authenticating.
A complete timeline traces the full loop — from URL entry through guard interception, login, and final render of the protected Pay component. The design separates concerns cleanly: business pages carry zero auth logic, the guard centralizes permission checks, and the login form uses `FormData` to avoid unnecessary re-renders. The entire mechanism relies on two implicit data channels: `localStorage` for the login flag and `location.state` for the user's original intent.
The `replace` vs `push` distinction is the single most overlooked detail in auth redirects, and getting it wrong creates a silent UX bug that QA often misses.
Carrying the entire `location` object rather than a bare path string is a defensive habit that costs nothing and saves coupon parameters, UTM tags, and deep-link state.
The `children` prop pattern in ProtectRoute is a textbook example of separation of concerns: the guard knows nothing about what it protects, and the page knows nothing about auth.
Using `FormData` for login is a pragmatic trade-off — it sacrifices real-time validation for simplicity, which is the right call when the form has two fields and no dynamic validation rules.