Authenticated Routes
Route context for auth
Inject authentication state into the router via context. This lets all routes access auth without prop-threading:
import { createRouter, createRootRouteWithContext } from "@domphy/router"
import { toState } from "@domphy/core"
interface AuthContext {
isAuthenticated: () => boolean
user: () => User | null
}
// Root route typed with context
const rootRoute = createRootRouteWithContext<AuthContext>()({
component: () => ({ div: "..." }),
})
// Provide context when creating the router
const auth = {
isAuthenticated: () => !!localStorage.getItem("token"),
user: () => JSON.parse(localStorage.getItem("user") ?? "null"),
}
const router = createRouter({
routeTree: rootRoute,
context: auth,
})Protecting a route with beforeLoad
Use beforeLoad to redirect unauthenticated users before the route renders:
import { createRoute, redirect } from "@domphy/router"
const dashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/dashboard",
beforeLoad: ({ context }) => {
if (!context.isAuthenticated()) {
throw redirect({ to: "/login", search: { returnTo: "/dashboard" } })
}
},
component: () => Dashboard,
})beforeLoad runs on every navigation to the route. Throwing redirect(...) cancels the navigation and redirects instead.
Redirect with return URL
After login, redirect back to the page the user was trying to reach:
const loginRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/login",
validateSearch: (search) => ({
returnTo: typeof search.returnTo === "string" ? search.returnTo : "/",
}),
component: () => LoginPage,
})
// In LoginPage:
const LoginPage = {
form: [
// ... form fields
{
button: "Log in",
onClick: async () => {
await login(credentials)
const returnTo = router.state.location.search.returnTo ?? "/"
router.navigate({ to: returnTo })
},
},
],
}Auth group route (layout route)
Create a pathless layout route that wraps all authenticated pages. Put the beforeLoad check once:
const authLayout = createRoute({
getParentRoute: () => rootRoute,
id: "_auth", // no path — just a guard
beforeLoad: ({ context }) => {
if (!context.isAuthenticated()) {
throw redirect({ to: "/login" })
}
},
component: () => ({
div: [
NavBar,
{ div: RouterOutlet }, // render child routes
],
}),
})
// All children are automatically protected
const dashboardRoute = createRoute({ getParentRoute: () => authLayout, path: "/dashboard" })
const settingsRoute = createRoute({ getParentRoute: () => authLayout, path: "/settings" })
const profileRoute = createRoute({ getParentRoute: () => authLayout, path: "/profile" })Role-based access control
Check roles in beforeLoad:
const adminRoute = createRoute({
getParentRoute: () => authLayout,
path: "/admin",
beforeLoad: ({ context }) => {
const user = context.user()
if (!user || user.role !== "admin") {
throw redirect({ to: "/", search: { error: "forbidden" } })
}
},
component: () => AdminPanel,
})For multiple roles:
function requireRole(...roles: string[]) {
return ({ context }: { context: AuthContext }) => {
const user = context.user()
if (!user || !roles.includes(user.role)) {
throw redirect({ to: "/" })
}
}
}
const editorRoute = createRoute({
getParentRoute: () => authLayout,
path: "/editor",
beforeLoad: requireRole("editor", "admin"),
component: () => EditorPage,
})Token refresh in loaders
Handle expired tokens before loading route data:
const protectedRoute = createRoute({
getParentRoute: () => authLayout,
path: "/data",
beforeLoad: async ({ context }) => {
// If token is expired, refresh before proceeding
if (isTokenExpired()) {
try {
await refreshToken()
} catch {
throw redirect({ to: "/login" })
}
}
},
loader: async () => {
// Token is now valid
return fetchProtectedData()
},
})Loading auth state asynchronously
When auth state is fetched from the server (not from localStorage), defer rendering until it's ready:
const authState = toState<{ user: User | null; loading: boolean }>({
user: null, loading: true,
})
// Fetch auth on app start
async function initAuth() {
try {
const user = await fetchCurrentUser()
authState.set({ user, loading: false })
} catch {
authState.set({ user: null, loading: false })
}
}
initAuth()
const App = {
div: (l) => {
const { loading } = authState.get(l)
if (loading) return { div: "Loading…" }
return RouterApp
},
}Then provide user from authState in the router context.
Logout
Clear auth state and redirect to login:
function logout() {
localStorage.removeItem("token")
localStorage.removeItem("user")
// Invalidate all cached query data
client.clear()
router.navigate({ to: "/login" })
}
const LogoutButton = {
button: "Log out",
onClick: logout,
}