T
TanStack2y ago
stormy-gold

Authentication Context Updates vs Redirect (Race Condition) & Maybe useRouteContext() is Static?!

Hey all! New to Tanstack Router here, and trying to follow the Authentication guide (https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes) and Authenticated Routes Context example (https://tanstack.com/router/latest/docs/framework/react/examples/authenticated-routes-context) but I'm running into a situation where, upon login/logout, my redirects (to Dashboard on login and Login on logout) beat the context update –if it is indeed actually updating– and the wrong info is rendered. My suspicion is that this is because the router's context isn't actually updating at all when it's supposed to?! That feels like I'm doing something wrong. In debugging this, I've been able to see that when my useAuth() context hook updates and its new state gets passed into the <RouterProvider /> context, a page component that calls useRouteContext() like the following never receives that new state: const { isLoading } = myRoute.useRouteContext({ select: ({ auth }) => ({ isLoading: auth.isLoading }) }); My page just shows its loading state forever. Similarly, it doesn't appear that the router ever learns that the user has changed within the auth state (to a user object or to null), which could explain why I see the wrong info in my intro paragraph above. I've attached some pared down code in comments below for diagnosis! Thanks so much for your help.
Authenticated Routes | TanStack Router 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 they try to access them. The route.beforeLoad Option
React Router Authenticated Routes Context Example | TanStack Router...
An example showing how to implement Authenticated Routes Context in React Router
11 Replies
fascinating-indigo
fascinating-indigo2y ago
I have the same issue there: #Context & protected routes
stormy-gold
stormy-goldOP2y ago
First, my Auth Context
// @/lib/auth.tsx
export interface AuthContextValue {
isAuthenticated: boolean;
isLoading: boolean;
setUser: Dispatch<SetStateAction<User | null>>;
user: User | null;
}

const AuthContext = createContext<AuthContextValue | null>(null);

export function AuthProvider(props: {
children: ReactNode;
}) {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [user, setUser] = useState<User | null>(() => loadUserFromStorage()); // Check localStorage for session first
const isAuthenticated = !!user;

useEffect(() => {
const fetchUser = async () => {
const user = await getCurrentSession(); // Fetch session from backend

setUser(user);
setIsLoading(false);
}

fetchUser();
}, []);

return (
<AuthContext.Provider
value={{
isAuthenticated,
isLoading,
setUser,
user
}}
>
{ props.children }
</AuthContext.Provider>
);
}

export function useAuth() {
const context = useContext(AuthContext);

if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}

return context;
}
// @/lib/auth.tsx
export interface AuthContextValue {
isAuthenticated: boolean;
isLoading: boolean;
setUser: Dispatch<SetStateAction<User | null>>;
user: User | null;
}

const AuthContext = createContext<AuthContextValue | null>(null);

export function AuthProvider(props: {
children: ReactNode;
}) {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [user, setUser] = useState<User | null>(() => loadUserFromStorage()); // Check localStorage for session first
const isAuthenticated = !!user;

useEffect(() => {
const fetchUser = async () => {
const user = await getCurrentSession(); // Fetch session from backend

setUser(user);
setIsLoading(false);
}

fetchUser();
}, []);

return (
<AuthContext.Provider
value={{
isAuthenticated,
isLoading,
setUser,
user
}}
>
{ props.children }
</AuthContext.Provider>
);
}

export function useAuth() {
const context = useContext(AuthContext);

if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}

return context;
}
Second: Routes, Router, and Context Provider Setup I've segmented my routes under two parents, one for the routes that you must be logged out to see (like /login), the other for routes that you must be logged in to access (in this demo, /home).
// @/App.tsx
type RootContext = {
auth: AuthContextValue;
queryClient: QueryClient;
}

const rootRoute = createRootRouteWithContext<RootContext>()({
component: RootLayout,
});

const unAuthenticatedRoute = createRoute({
getParentRoute: () => rootRoute,
id: "unauthenticated",
beforeLoad: ({ context }) => {
if (context.auth.isAuthenticated) {
throw redirect({
to: homeRoute.to,
});
}
},
component: LoggedOutLayout,
});

const authenticatedRoute = createRoute({
getParentRoute: () => rootRoute,
id: "authenticated",
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: loginRoute.to,
search: {
redirect: location.href,
},
});
}
},
component: LoggedInLayout,
});

// ... Define loginRoute and homeRoute with parents here

const rootRoute.addChildren([
unAuthenticatedRoute.addChildren([
loginRoute,
]),
authenticatedRoute.addChildren([
homeRoute,
]),
]);

const queryClient = new QueryClient();

const router = createRouter({
context: {
auth: undefined!,
queryClient,
},
defaultPreload: "intent",
defaultPreloadStaleTime: 0,
routeTree,
});

declare module "@tanstack/react-router" {
interface Register {
router: typeof router
}
}

function InnerApp() {
const auth = useAuth();
console.log('useAuth() updated:', auth); // USED TO DEBUG BELOW

return (
<RouterProvider router={ router } context={{ auth }} />
);
}


export default function App() {
return (
<AuthProvider>
<QueryClientProvider client={ queryClient }>
<InnerApp />
</QueryClientProvider>
</AuthProvider>
);
}
// @/App.tsx
type RootContext = {
auth: AuthContextValue;
queryClient: QueryClient;
}

const rootRoute = createRootRouteWithContext<RootContext>()({
component: RootLayout,
});

const unAuthenticatedRoute = createRoute({
getParentRoute: () => rootRoute,
id: "unauthenticated",
beforeLoad: ({ context }) => {
if (context.auth.isAuthenticated) {
throw redirect({
to: homeRoute.to,
});
}
},
component: LoggedOutLayout,
});

const authenticatedRoute = createRoute({
getParentRoute: () => rootRoute,
id: "authenticated",
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: loginRoute.to,
search: {
redirect: location.href,
},
});
}
},
component: LoggedInLayout,
});

// ... Define loginRoute and homeRoute with parents here

const rootRoute.addChildren([
unAuthenticatedRoute.addChildren([
loginRoute,
]),
authenticatedRoute.addChildren([
homeRoute,
]),
]);

const queryClient = new QueryClient();

const router = createRouter({
context: {
auth: undefined!,
queryClient,
},
defaultPreload: "intent",
defaultPreloadStaleTime: 0,
routeTree,
});

declare module "@tanstack/react-router" {
interface Register {
router: typeof router
}
}

function InnerApp() {
const auth = useAuth();
console.log('useAuth() updated:', auth); // USED TO DEBUG BELOW

return (
<RouterProvider router={ router } context={{ auth }} />
);
}


export default function App() {
return (
<AuthProvider>
<QueryClientProvider client={ queryClient }>
<InnerApp />
</QueryClientProvider>
</AuthProvider>
);
}
fascinating-indigo
fascinating-indigo2y ago
In fact, the context is setted by default to default... then, it updates but this is a big issue for authenticated routes
stormy-gold
stormy-goldOP2y ago
Finally, attempting to utilize my auth context in a rendered component
// @/RootLayout.tsx
export default function RootLayout() {
const { isLoading } = rootRoute.useRouteContext({
select: ({ auth }) => ({ isLoading: auth.isLoading }),
});
console.log('RootLayout re-rendered with isLoading:', isLoading); // USED TO DEBUG BELOW

if (isLoading) {
return (
<div>Loading...</div>
);
}

return (
<Outlet />
);
}
// @/RootLayout.tsx
export default function RootLayout() {
const { isLoading } = rootRoute.useRouteContext({
select: ({ auth }) => ({ isLoading: auth.isLoading }),
});
console.log('RootLayout re-rendered with isLoading:', isLoading); // USED TO DEBUG BELOW

if (isLoading) {
return (
<div>Loading...</div>
);
}

return (
<Outlet />
);
}
💻 The Result Initially, the console prints:
useAuth() updated: { isAuthenticated: false, isLoading: true ... }
RootLayout re-rendered with isLoading: true
useAuth() updated: { isAuthenticated: false, isLoading: true ... }
RootLayout re-rendered with isLoading: true
Then the Auth Provider's useEffect() runs to fetch the user (still signed out, still null) from the API, and the console prints:
useAuth() updated: { isAuthenticated: false, isLoading: false ... }
RootLayout re-rendered with isLoading: true
useAuth() updated: { isAuthenticated: false, isLoading: false ... }
RootLayout re-rendered with isLoading: true
And the browser just shows "Loading...", because even though useAuth() knows isLoading is false, and its result is passed into <RouterProvider />'s context, isLoading never becomes false in useRouteContext() 😢 Oh! I almost forgot! The above is one blocking issue, but it's not the race condition I'm experiencing, it's just in the way! I have a sneaking suspicion that if I can get the root layout to show a loading state whenever the user changes from signed out to signed in and vice versa, this race condition might just go away. But I also suspect that if I can solve the fact that this useRouteContext() isn't updating, the components that need user from it will just work instead of being broken until a file change or browser refresh cause a rerender @Sean Cassiere I saw you were helping @wailroth earlier with their recent post about something potentially similar – does having a second example help hone in on what might be going on here? Popping back in to say that if I switch all my useRouteContext()s for useAuth()s, taking the state from my AuthProvider directly instead of through the RouterProvider (which again, is being fed that exact same state), everything works perfectly!
fascinating-indigo
fascinating-indigo2y ago
I think I should do the same so where are you calling it? because thing like
export const Route = createFileRoute('/account')({
beforeLoad: () => {
const context = useAuth();
if (!context.user) {
redirect({to:"/auth/login", params: {error: "access_denied"}});
}
},
component: () => AccountComponent(),
});
export const Route = createFileRoute('/account')({
beforeLoad: () => {
const context = useAuth();
if (!context.user) {
redirect({to:"/auth/login", params: {error: "access_denied"}});
}
},
component: () => AccountComponent(),
});
should not work, useAuth is a react hook
stormy-gold
stormy-goldOP2y ago
Ah, no I'm not doing mine in createFileRoute(), I actually found that my authenticated and unauthenticated route parents seem to get context.auth.isAuthenticated just fine... which is even more strange I'm calling useAuth() in the components the route mounts. In my code samples above, that means my third/final code block right before the console logs part – I replaced const { isLoading } = rootRoute.useRouteContext({... with const { isLoading } = useAuth() to skip over the router context and go straight to the auth context for that data
fascinating-indigo
fascinating-indigo2y ago
have you an example of that ? i'm not sure i'm well understanding
stormy-gold
stormy-goldOP2y ago
// @/RootLayout.tsx
export default function RootLayout() {
const { isLoading } = useAuth();

if (isLoading) {
return (
<div>Loading...</div>
);
}

return (
<Outlet />
);
}
// @/RootLayout.tsx
export default function RootLayout() {
const { isLoading } = useAuth();

if (isLoading) {
return (
<div>Loading...</div>
);
}

return (
<Outlet />
);
}
Now, this root component will show a loading message until my Auth Provider is done fetching the user from the API in its useEffect(), that result goes into the Auth Provider's context, and useAuth() has access to it here 🙂
fascinating-indigo
fascinating-indigo2y ago
because the major issue I have is to protect my routes; when I redirect an user from a link, there is not issues. But when I access to a route manually, like in #Context & protected routes , the context is not the same so i'm a little bit annoyed by that
stormy-gold
stormy-goldOP2y ago
Interesting, I don't know if you saw above but I have an authenticatedRoute created that doesn't have a path, but it does check for signed-in status and redirect if it doesn't find it. Then any page I want to have use that behavior, I set getParentRoute: () => authenticatedRoute And in that authenticatedRoute, for whatever reason, context.auth.isAuthenticated appears to be reliable
fascinating-indigo
fascinating-indigo2y ago
okay; I should do it like you I think How do you implement this authenticatedRoute ? with this:
const rootRoute.addChildren([
unAuthenticatedRoute.addChildren([
loginRoute,
]),
authenticatedRoute.addChildren([
homeRoute,
]),
]);
const rootRoute.addChildren([
unAuthenticatedRoute.addChildren([
loginRoute,
]),
authenticatedRoute.addChildren([
homeRoute,
]),
]);
? Btw i'm using fileBased routes when you follow this: https://codesandbox.io/p/devbox/github/tanstack/router/tree/main/examples/react/authenticated-routes-context?embed=1&file=%2Fsrc%2Fmain.tsx&theme=dark you see that they are doing it like me

Did you find this page helpful?