three gates around every navigation
A route is more than a component. The router runs a lifecycle
around each navigation: beforeLoad can throw redirect() (the auth
gate), a blocker can pause or cancel the transition (the
unsaved-changes gate), and masking decides which URL lands in history vs. which route
actually renders. The order matters: block check → auth check →
commit URL → render.
| pattern | gate | API | what it can do |
|---|---|---|---|
| Authenticated route | before navigation commits | beforeLoad + throw redirect() |
abort → send to /login, remember the target, redirect back after auth |
| Navigation blocking | before navigation commits | useBlocker({ shouldBlockFn, withResolver }) |
pause → show "unsaved changes?" UI → proceed() or reset() |
| Location masking | at commit (history write) | <Link mask={{ to }}> / createRouteMask() |
render route A, but persist a clean URL B to history + the address bar |
1 · the auth + blocking simulator (edit me)
2 · Babel compiles JSX → element tree
<AuthDemo/> becomes React.createElement(AuthDemo).
navigate() runs the two gates in order — blocker first (pause + remember
target), then auth (redirect to /login + remember destination). The
blocker's proceed/reset map to forceNavigate/
cancelBlock.
// (hit "compile & render" to see Babel's output)
3 · live React (the gate pipeline, proven)
Click Dashboard while logged out → you're sent to
/login and the destination is remembered. Login
sends you back. In the Editor, toggle unsaved changes and try
to leave — navigation pauses. Force navigate resolves the
block. The gold-check walks all six steps automatically.
route: — · user: — · gold: home→login→dashboard→editor→block→force→home
intent → pattern
| intent | pattern | why |
|---|---|---|
| require login for a route | beforeLoad + throw redirect({'{'}to:'/login', search:{'{'}redirect: location.pathname{'}'}{'}'}) |
redirect aborts the navigation; search param remembers the target |
| redirect back after login | read search.redirect on the login route → navigate({'{'}to: redirect{'}'}) |
the "remember where I was going" round-trip |
| block leaving with unsaved changes | useBlocker({'{'}shouldBlockFn: () ={'>'} isDirty, withResolver: true{'}'}) |
returns {'{'}proceed, reset, status{'}'} — drive a custom dialog |
| also block tab close / reload | enableBeforeUnload: isDirty on the blocker |
registers the native beforeunload handler conditionally |
| show a clean URL, render a different route | <Link to="/photos/$id/modal" mask={'{'}to:'/photos/$id'{'}'}> |
address bar + history show the mask; router renders the real route |
| shareable masked URLs app-wide | createRouteMask({'{'}routeTree, from, to{'}'}) → router routeMasks |
declare once; every matching navigate is masked |