T
TanStack2mo ago
equal-aqua

Idiomatic ways to handle route-guarding

This doc led me to using router contexts to be able to pass authentication context to the router https://tanstack.com/router/v1/docs/framework/react/guide/authenticated-routes#authentication-using-react-contexthooks However I'm running into an issue. Here's my route:
export const Route = createFileRoute("/dashboard")({
beforeLoad: ({ context, location }) => {
// Check authentication using context
console.log("[Dashboard Route Guard] Checking authentication:", {
path: location.pathname,
isAuthenticated: context.auth.isAuthenticated,
hasUser: !!context.auth.user,
userEmail: context.auth.user?.email,
hasJWT: !!context.auth.jwt,
jwtPreview: context.auth.jwt
? `${context.auth.jwt.substring(0, 20)}...`
: null,
context,
});

if (!context.auth.isAuthenticated || !context.auth.jwt) {
console.log(
"[Dashboard Route Guard] Not authenticated, redirecting to signin",
);
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw redirect({
to: "/signin",
search: {
redirect: location.href,
},
});
}
export const Route = createFileRoute("/dashboard")({
beforeLoad: ({ context, location }) => {
// Check authentication using context
console.log("[Dashboard Route Guard] Checking authentication:", {
path: location.pathname,
isAuthenticated: context.auth.isAuthenticated,
hasUser: !!context.auth.user,
userEmail: context.auth.user?.email,
hasJWT: !!context.auth.jwt,
jwtPreview: context.auth.jwt
? `${context.auth.jwt.substring(0, 20)}...`
: null,
context,
});

if (!context.auth.isAuthenticated || !context.auth.jwt) {
console.log(
"[Dashboard Route Guard] Not authenticated, redirecting to signin",
);
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw redirect({
to: "/signin",
search: {
redirect: location.href,
},
});
}
On the server, I see:
[web] [Dashboard Route Guard] Checking authentication: {
[web] path: '/dashboard',
[web] isAuthenticated: false,
[web] hasUser: false,
[web] userEmail: undefined,
[web] hasJWT: false,
[web] jwtPreview: null,
[web] context: {
[web] queryClient: QueryClient {},
[web] auth: {
[web] isAuthenticated: false,
[web] user: null,
[web] jwt: null,
[web] authFlow: 'logged-out'
[web] }
[web] }
[web] }
[web] [Dashboard Route Guard] Not authenticated, redirecting to signin
[web] [Signin Route] Context: {
[web] queryClient: QueryClient {},
[web] auth: {
[web] isAuthenticated: false,
[web] user: null,
[web] jwt: null,
[web] authFlow: 'logged-out'
[web] }
[web] }
[web] [AppWithAuth] Waiting for store hydration...
[web] [AppWithAuth] Waiting for store hydration...
[web] [Dashboard Route Guard] Checking authentication: {
[web] path: '/dashboard',
[web] isAuthenticated: false,
[web] hasUser: false,
[web] userEmail: undefined,
[web] hasJWT: false,
[web] jwtPreview: null,
[web] context: {
[web] queryClient: QueryClient {},
[web] auth: {
[web] isAuthenticated: false,
[web] user: null,
[web] jwt: null,
[web] authFlow: 'logged-out'
[web] }
[web] }
[web] }
[web] [Dashboard Route Guard] Not authenticated, redirecting to signin
[web] [Signin Route] Context: {
[web] queryClient: QueryClient {},
[web] auth: {
[web] isAuthenticated: false,
[web] user: null,
[web] jwt: null,
[web] authFlow: 'logged-out'
[web] }
[web] }
[web] [AppWithAuth] Waiting for store hydration...
[web] [AppWithAuth] Waiting for store hydration...
Authenticated Routes | TanStack Router React Docs
Authentication is an extremely common requirement for web applications. In this guide, we'll walk through how to use TanStack Router to build protected routes, and how to redirect users to login if th...
7 Replies
equal-aqua
equal-aquaOP2mo ago
My AppWithAuth code:
export function AppWithAuth({ children }: AppWithAuthProps) {
const router = useRouter();

const authStore = useAuthStore();
const [hasHydrated, setHasHydrated] = useState(false);

useEffect(() => {
// Use the Zustand persist API to check hydration
const unsubscribe = useAuthStore.persist.onFinishHydration(() => {
console.log("[AppWithAuth] Store hydrated");
setHasHydrated(true);
});

// If already hydrated, set immediately
if (useAuthStore.persist.hasHydrated()) {
setHasHydrated(true);
}

return unsubscribe;
}, []); // Run once on mount

// Update router context when auth state changes
useEffect(() => {
if (!hasHydrated) return;

const auth = {
isAuthenticated:
!!authStore.jwt &&
!!authStore.user &&
authStore.authFlow === "logged-in",
user: authStore.user,
jwt: authStore.jwt,
authFlow: authStore.authFlow,
};

console.log("[AppWithAuth] Updating router context with auth state:", {
isAuthenticated: auth.isAuthenticated,
hasUser: !!auth.user,
userEmail: auth.user?.email,
hasJWT: !!auth.jwt,
authFlow: auth.authFlow,
});

// Update the router's context
const context = router.options.context as RouterContext;
context.auth = auth as RouterContext["auth"];

// Force router to re-evaluate routes with new context
void router.invalidate();
}, [router, authStore.jwt, authStore.user, authStore.authFlow, hasHydrated]);

// Show loading state while store hydrates
if (!hasHydrated) {
console.log("[AppWithAuth] Waiting for store hydration...");
return (
<div className="flex min-h-screen items-center justify-center">
<div
className={`
h-8 w-8 animate-spin rounded-full border-b-2 border-primary
`}
></div>
</div>
);
}

console.log("[AppWithAuth] Rendering RouterProvider with hydrated auth");

return <>{children}</>;
}
export function AppWithAuth({ children }: AppWithAuthProps) {
const router = useRouter();

const authStore = useAuthStore();
const [hasHydrated, setHasHydrated] = useState(false);

useEffect(() => {
// Use the Zustand persist API to check hydration
const unsubscribe = useAuthStore.persist.onFinishHydration(() => {
console.log("[AppWithAuth] Store hydrated");
setHasHydrated(true);
});

// If already hydrated, set immediately
if (useAuthStore.persist.hasHydrated()) {
setHasHydrated(true);
}

return unsubscribe;
}, []); // Run once on mount

// Update router context when auth state changes
useEffect(() => {
if (!hasHydrated) return;

const auth = {
isAuthenticated:
!!authStore.jwt &&
!!authStore.user &&
authStore.authFlow === "logged-in",
user: authStore.user,
jwt: authStore.jwt,
authFlow: authStore.authFlow,
};

console.log("[AppWithAuth] Updating router context with auth state:", {
isAuthenticated: auth.isAuthenticated,
hasUser: !!auth.user,
userEmail: auth.user?.email,
hasJWT: !!auth.jwt,
authFlow: auth.authFlow,
});

// Update the router's context
const context = router.options.context as RouterContext;
context.auth = auth as RouterContext["auth"];

// Force router to re-evaluate routes with new context
void router.invalidate();
}, [router, authStore.jwt, authStore.user, authStore.authFlow, hasHydrated]);

// Show loading state while store hydrates
if (!hasHydrated) {
console.log("[AppWithAuth] Waiting for store hydration...");
return (
<div className="flex min-h-screen items-center justify-center">
<div
className={`
h-8 w-8 animate-spin rounded-full border-b-2 border-primary
`}
></div>
</div>
);
}

console.log("[AppWithAuth] Rendering RouterProvider with hydrated auth");

return <>{children}</>;
}
In my app.tsx, I wrap the AppWithAuth:
<AuthProvider>
<AppWithAuth>
<AuthProvider>
<AppWithAuth>
My auth store is a simple zustand store. Perhaps this router context isn't actually visible by the server? Do I need to use cookies for this (that's what my intuition is telling me, but it would be magical for the server to be aware of the client side router context)? currently every time I navigate to a /dashboard (or any children routes), it always jumps to the /signin page, then jumps right back to the target page because of the throw redirect
beforeLoad: ({ context, location }) => {
if (typeof window === "undefined") {
return;
}
beforeLoad: ({ context, location }) => {
if (typeof window === "undefined") {
return;
}
adding this to the top works, zero cookies needed, but kinda feels wrong so the check is all client side, which feels fine imho
protestant-coral
protestant-coral2mo ago
where is the AuthProvider rendered? inside the root route? anyhow, you cannot (currently) pass react hook values into router context
equal-aqua
equal-aquaOP2mo ago
yes its inside the root route
equal-aqua
equal-aquaOP2mo ago
How should I think about this comment on the docs:
export const router = createRouter({
routeTree,
context: {
// auth will initially be undefined
// We'll be passing down the auth state from within a React component
auth: undefined!,
},
})
export const router = createRouter({
routeTree,
context: {
// auth will initially be undefined
// We'll be passing down the auth state from within a React component
auth: undefined!,
},
})
from here https://tanstack.com/router/v1/docs/framework/react/guide/authenticated-routes#authentication-using-react-contexthooks
Authenticated Routes | TanStack Router React Docs
Authentication is an extremely common requirement for web applications. In this guide, we'll walk through how to use TanStack Router to build protected routes, and how to redirect users to login if th...
equal-aqua
equal-aquaOP2mo ago
Oh I see, perhaps I need to put the auth context above the root router, then pass it in as context
protestant-coral
protestant-coral2mo ago
thats not possible with start though
equal-aqua
equal-aquaOP2mo ago
ya makes sense now

Did you find this page helpful?