T
TanStack•11mo ago
harsh-harlequin

authenticated routes

I am implementing authenticated routes and was following the guide in https://tanstack.com/router/v1/docs/framework/react/guide/authenticated-routes#authentication-using-react-contexthooks where it talks about authenticated routes for auth state stored in a Context. It all makes sense but I'm struggling with one thing - My setup My react Context gets the state for whether the user is authenticated or not by making an api call to /user and if the response is valid and the user details are fetched successfully, it stores that state in the context as user. This is the state I am using to validate whether the user is logged in or not. Question Given my auth state depends on the user state that is going to be fetched, I don't want the check in beforeLoad to happen before that fetch and setting of state is complete. My current code is as follows but even if the user is logged in, it goes to login.
export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context, location }) => {
if (!context.user) {
throw redirect({
to: '/login',
search: {
redirect: location.href,
},
})
}
},
})
export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context, location }) => {
if (!context.user) {
throw redirect({
to: '/login',
search: {
redirect: location.href,
},
})
}
},
})
The logic of the behavior makes sense but what is the recommended way to handle the isLoading state in such a setup?
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...
75 Replies
automatic-azure
automatic-azure•11mo ago
You did not show where you make the request for the user i'd use @tanstack/query to make the request for the user (so I can control how it gets cached), and I would fetch the user in the /_authenticated's beforeLoad, then adding it to the router's context by returning an object from beforeLoad
harsh-harlequin
harsh-harlequinOP•11mo ago
I make the request in the user context. The nesting looks as follows -
<UserContextProvider>
<RouterProvider>
<RestOfTheApp />
</RouterProvider>
</UserContextProvider>
<UserContextProvider>
<RouterProvider>
<RestOfTheApp />
</RouterProvider>
</UserContextProvider>
I am using tanstack query for the fetching and caching. My main issue is that while the query is loading, the beforeLoad in _authenticated is already completed and it shows no one is logged in. Could you give me an example snipped for what you are suggesting? I can't call a hook in beforeLoad so I am bit confused by what you said Also when I logout, the beforeLoad doesn't re-compute so how should I be handling that?
automatic-azure
automatic-azure•11mo ago
router.invalidate()
continuing-cyan
continuing-cyan•11mo ago
you can supply hook values via the routerprovider <RouterProvider context={{foo: 'bar'}>
harsh-harlequin
harsh-harlequinOP•11mo ago
I actually do provide it there My issue was that the GET /user has not finished fetching when I'm passing the values in <RouterProvider context={{ user: UserData }}> And so I'm passing in null Then when it makes the check in beforeLoad, it redirects to login (even though it is still fetching /user) For now I'm not rendering the children inside my <UserProvider> until the /user query has finished fetching. This way when I set the user state in the <RouterProvider> I know it has been fetched. Does that make sense or is there a simpler way to do this?
continuing-cyan
continuing-cyan•11mo ago
you could pass a promise in there and await that promise in the beforeLoad you could also invert the logic and not use a UserContextProvider but instead use a loader / beforeLoade in __root
harsh-harlequin
harsh-harlequinOP•11mo ago
Both interesting approaches! 1. Passing in a promise - I was thinking about having this state available across the app and hence using a context with state in it. Passing a promise would be without TSQ I'm guessing? Like just pass in an axios fetch and let it resolve and then take care of the auth state? In this case, if I want that state to be available, I'd just have the user context inside the authenticated routes with another TSQ fetch made? 2. Use a loader/beforeLoad in __root - Where can I understand loaders better? Also same issue of wanting this user state across my app as above. Also how would I access this in the _authenticated beforeLoad? Not sure if I'm missing something obvious here but hope my questions make sense! Thanks for the help!
continuing-cyan
continuing-cyan•11mo ago
1. you could still use a context, you would just have to resolve said promise to signal that now the data is available. you don't have to do this for react, but for router, as router is not living in react world. 2. e.g here https://tanstack.com/router/v1/docs/framework/react/guide/data-loading and here https://tanstack.com/router/v1/docs/framework/react/guide/external-data-loading in 2 you could still render a provider inside your _root route component to provide the value to all downstream react components if you return something from a beforeLoad, it will be put into the router context and child routes will be able to access it in e.g. beforeLoad (passed in as an arg)
harsh-harlequin
harsh-harlequinOP•11mo ago
ah makes sense! in (1), I'd be making that GET /user twice though right? One for the promise and one for the context?
continuing-cyan
continuing-cyan•11mo ago
not necessarily
// pseudocode
let resolve
const promise = new Promise(r => {resolve = r})
fetch().then(resolve)
// pass promise into router context
// pseudocode
let resolve
const promise = new Promise(r => {resolve = r})
fetch().then(resolve)
// pass promise into router context
I would go for (2)
harsh-harlequin
harsh-harlequinOP•11mo ago
Right. I guess you're suggesting using the router context instead of my own user context here in (2)?
automatic-azure
automatic-azure•11mo ago
Why this when he could pass the promise resulting from fetch()?
continuing-cyan
continuing-cyan•11mo ago
also possible, sure you need to be aware of the two worlds. react vs router. a router context is not a react context (nor vice versa) you can access the router context from react (via useRouteContext)
harsh-harlequin
harsh-harlequinOP•11mo ago
right that makes sense now
continuing-cyan
continuing-cyan•11mo ago
maybe you can just call https://tanstack.com/query/v5/docs/reference/QueryClient/#queryclientensurequerydata in the beforeLoad to ensure that you have a logged in user. and use TSQ as your cache you can then either read that value from query, or from the route context if you choose to return it from beforeLoad btw, beforeLoad is executed before each navigation. so if this is not cached, it would call your server for each navigation.
harsh-harlequin
harsh-harlequinOP•11mo ago
I got a bit confused there... sorry https://discord.com/channels/719702312431386674/1330884061144551427/1330986250642919556 could you first explain how this would be possible?
continuing-cyan
continuing-cyan•11mo ago
where is this pointing to?
harsh-harlequin
harsh-harlequinOP•11mo ago
Or maybe I can just explain what I'm thinking right now 1. In the beforeLoad in the __root, get the user data (await it) and return it. 2. In the _authenticated.tsx beforeLoad, check the user data (which comes in as args) for whether someone is authenticated or not. This makes sense to me Now I want to also have this data accessible elsewhere. This is why I wanted a UserProvider which you are saying I can just wrap around the root component and inside the provider just store the user data (with another fetch) as react state. However, I didn't realize that "beforeLoad is executed before each navigation. so if this is not cached, it would call your server for each navigation". Not sure how to best handle that...
continuing-cyan
continuing-cyan•11mo ago
use TanStack query for this as a cache
harsh-harlequin
harsh-harlequinOP•11mo ago
what would the pseudo code in the __root beforeLoad look like? That's where I'm confused If going with TSQ^
continuing-cyan
continuing-cyan•11mo ago
const data = await queryClient.ensureQueryData(opts)
if(!data.user) {
throw redirect(...)
}
const data = await queryClient.ensureQueryData(opts)
if(!data.user) {
throw redirect(...)
}
harsh-harlequin
harsh-harlequinOP•11mo ago
I am also on v4 if that matters
continuing-cyan
continuing-cyan•11mo ago
don't think you need more than that
harsh-harlequin
harsh-harlequinOP•11mo ago
dont think ensureQueryData is in v4 😓
continuing-cyan
continuing-cyan•11mo ago
QueryClient | TanStack Query Docs
QueryClient The QueryClient can be used to interact with a cache: tsx import { QueryClient } from '@tanstack/react-query' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime:...
continuing-cyan
continuing-cyan•11mo ago
it is
harsh-harlequin
harsh-harlequinOP•11mo ago
ok got it! thank you! Will work with this
automatic-azure
automatic-azure•11mo ago
I also want to mention that this should be in _authenticated/, you dont need anything in __root
continuing-cyan
continuing-cyan•11mo ago
absolutely right
automatic-azure
automatic-azure•11mo ago
unless you want to pass the user to the context of routes outside of _authenticated/
continuing-cyan
continuing-cyan•11mo ago
otherwise you cant login
harsh-harlequin
harsh-harlequinOP•11mo ago
right! And for logout, would the queryCache invalidation be enough?
continuing-cyan
continuing-cyan•11mo ago
you would also need to router.invalidate()
harsh-harlequin
harsh-harlequinOP•11mo ago
Or is the router.invalidate needed? right
continuing-cyan
continuing-cyan•11mo ago
to force reexecution of beforeLoad
harsh-harlequin
harsh-harlequinOP•11mo ago
sweet
continuing-cyan
continuing-cyan•11mo ago
and thus redirecting
harsh-harlequin
harsh-harlequinOP•11mo ago
🤞 Quick question - the queryClient should be passed into the router context I'm guessing? To be accessible in beforeLoad?
continuing-cyan
continuing-cyan•11mo ago
can be could also just be imported only matters if you want to ever inject another instance e.g. during testing
harsh-harlequin
harsh-harlequinOP•11mo ago
I can't call const queryClient = useQueryClient(); in the beforeLoad right? How would I import it?
continuing-cyan
continuing-cyan•11mo ago
just import it 🤪 if you have export queryClient = ... somewhere you would just import that instance but using router context is much cleaner
harsh-harlequin
harsh-harlequinOP•11mo ago
I see 😅 . I will just go with the router context if that is the better way to do it. I'm guessing that is how you would do it?
continuing-cyan
continuing-cyan•11mo ago
yes and this is also how we do it in the react query based router examples
harsh-harlequin
harsh-harlequinOP•11mo ago
got it this all works! thank you so much just one last (hopefully) follow up - in my _unauthenticated.tsx beforeLoad, I have -
const data = await queryClient.ensureQueryData(params)
if (data.email) {
throw redirect({
to: "/",
});
}
const data = await queryClient.ensureQueryData(params)
if (data.email) {
throw redirect({
to: "/",
});
}
however, this isn't working for some reason... I am still able to see the /login route even when I'm logged in
continuing-cyan
continuing-cyan•11mo ago
would need a minimal complete example can you fork an existing one from stackblitz and modify accordingly ?
harsh-harlequin
harsh-harlequinOP•11mo ago
I found the issue. It was quite stupid. I was calling the throw in a try catch and so it wasn't working... But I'm still unclear on how to handle some patterns
harsh-harlequin
harsh-harlequinOP•11mo ago
No description
harsh-harlequin
harsh-harlequinOP•11mo ago
This is what I'm looking to implement. The thing that is becoming tough to handle is that in my case a 403 error means not logged in vs other errors should have an error component. Since there could be an error thrown in the beforeLoad, I have a try catch. However, I want to also throw redirect
automatic-azure
automatic-azure•11mo ago
are you using axios by chance?
harsh-harlequin
harsh-harlequinOP•11mo ago
yes iam
automatic-azure
automatic-azure•11mo ago
thought so, as normal fetch does not error on non-200 response codes so, 403 means you want to throw redirect and anything else should rethrow and be caught by the router's error component, right?
harsh-harlequin
harsh-harlequinOP•11mo ago
Ohh
automatic-azure
automatic-azure•11mo ago
try {
// ...
} catch (error) {
if (error.status === 403) throw redirect(/* ... */)

throw error
}
try {
// ...
} catch (error) {
if (error.status === 403) throw redirect(/* ... */)

throw error
}
harsh-harlequin
harsh-harlequinOP•11mo ago
yes correct But in the _unauthenticated case where there isn't an error, I want to redirect to /
automatic-azure
automatic-azure•11mo ago
so wait you want the user, even in the case where you are not authenticated? i think we are cycling back to what I said initially lmao
// __root.tsx
beforeLoad: async () => {
try {
const res = await axios.get(...)

return { user: res.data }
} catch (error) {
if (error.status !== 403) throw error
}
}

// _authenticated/
beforeLoad: ({ context }) => {
if (!context.user) throw redirect(...)
}
// __root.tsx
beforeLoad: async () => {
try {
const res = await axios.get(...)

return { user: res.data }
} catch (error) {
if (error.status !== 403) throw error
}
}

// _authenticated/
beforeLoad: ({ context }) => {
if (!context.user) throw redirect(...)
}
type user in your router context as User | null and pass null as default you could also make it optional, and work with undefined, but I prefer null to signify that it is not there as opposed to, something went wrong and it is not defined
harsh-harlequin
harsh-harlequinOP•11mo ago
Also, just in case it wasn't clear I have my /login route under _unauthenticated.tsx and so if a user is returned, I want it to redirect to /
automatic-azure
automatic-azure•11mo ago
// _unauthenticated/
beforeLoad: ({ context }) => {
if (context.user) throw redirect(...) // To '/'
}
// _unauthenticated/
beforeLoad: ({ context }) => {
if (context.user) throw redirect(...) // To '/'
}
harsh-harlequin
harsh-harlequinOP•11mo ago
I see what you're doing! You're decoupling the beforeLoads.. And just __root handles the axios GET
automatic-azure
automatic-azure•11mo ago
i'd rather do this just in the auth routes tho (login/register etc.), as you might want public routes that logged in users want to access
harsh-harlequin
harsh-harlequinOP•11mo ago
Ya makes sense Just one question and again this might be stupid
automatic-azure
automatic-azure•11mo ago
but, again, make the request using tanstack query, to cache the user and avoid other requests for it np, ask away
harsh-harlequin
harsh-harlequinOP•11mo ago
In the __root beforeLoad you return user as User | null. How does the beforeLoad in _authenticated, for example, know the type of the context there?
automatic-azure
automatic-azure•11mo ago
it's going to show as User | null the type wont change
harsh-harlequin
harsh-harlequinOP•11mo ago
yes but how does it infer the type there? I'm not defining the type explicitly anywhere right?
automatic-azure
automatic-azure•11mo ago
it doesnt? you need to specify it when you pass it to createRootRouteWithContext
harsh-harlequin
harsh-harlequinOP•11mo ago
Ah I see
automatic-azure
automatic-azure•11mo ago
thats why I told you to type it as User | null, you give the type for it
harsh-harlequin
harsh-harlequinOP•11mo ago
Makes sense
automatic-azure
automatic-azure•11mo ago
nothing gets inferred
harsh-harlequin
harsh-harlequinOP•11mo ago
Perfect. All of it makes sense now I'm curious - does this setup seem wrong? Or inefficient
automatic-azure
automatic-azure•11mo ago
not at all, this is pretty much the recommended way
harsh-harlequin
harsh-harlequinOP•11mo ago
Even how I have my apis set up? where I do a /user to check for login or not
automatic-azure
automatic-azure•11mo ago
that's not odd, usually when using REST apis you'd have something like a /me API endpoint to give you user profile which is pretty much same as what you are doing
harsh-harlequin
harsh-harlequinOP•11mo ago
yes, makes sense Dude thanks so much! Just implementing it all now. Hopefully I don't come back to this thread with more Qs lol
automatic-azure
automatic-azure•11mo ago
Gl

Did you find this page helpful?