React Router Auth Guards: The children Pattern, Redirects, and History Cleanup
The children-based guard pattern decouples access control from page components, so a single ProtectRoute can wrap any number of protected routes without duplicating logic. Pairing replace on both the guard redirect and the post-login navigation eliminates the back-button loop that plagues naive login flows.
ProtectRoute wraps any component with an authentication check, reading a login flag from localStorage and returning a <Navigate> redirect when the check fails. The redirect carries the original path in location.state so the login form can send the user back to exactly where they were headed. Login uses useNavigate with replace: true to overwrite the login page's history entry, preventing the back button from landing on an already-authenticated login screen that would immediately redirect again. The same guard pattern extends beyond authentication to any pre-entry check: role permissions, feature flags, or A/B experiment routing.
Using localStorage for auth state is convenient for demos but sidesteps the token-refresh and session-expiry problems that real production guards must handle.
The children pattern makes ProtectRoute a transparent wrapper: it imposes no props contract on the components it protects, so any page can be dropped inside without modification.
replace on both sides of the login flow is the detail that prevents the frustrating back-button loop; omitting it is a common mistake in hand-rolled auth routing.
The same guard structure applies to any pre-render check, not just auth. A feature-flag guard or an A/B test splitter would follow the identical children + conditional Navigate pattern.