T
TanStack•8mo ago
optimistic-gold

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
vicious-gold
vicious-gold•8mo 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
optimistic-gold
optimistic-goldOP•8mo 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?
vicious-gold
vicious-gold•8mo ago
router.invalidate()
other-emerald
other-emerald•8mo ago
you can supply hook values via the routerprovider <RouterProvider context={{foo: 'bar'}>
optimistic-gold
optimistic-goldOP•8mo 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?
other-emerald
other-emerald•8mo 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
optimistic-gold
optimistic-goldOP•8mo 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!
other-emerald
other-emerald•8mo 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)
optimistic-gold
optimistic-goldOP•8mo 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?
other-emerald
other-emerald•8mo 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)
optimistic-gold
optimistic-goldOP•8mo ago
Right. I guess you're suggesting using the router context instead of my own user context here in (2)?
vicious-gold
vicious-gold•8mo ago
Why this when he could pass the promise resulting from fetch()?
other-emerald
other-emerald•8mo 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)
optimistic-gold
optimistic-goldOP•8mo ago
right that makes sense now
other-emerald
other-emerald•8mo 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.
optimistic-gold
optimistic-goldOP•8mo ago
I got a bit confused there... sorry https://discord.com/channels/719702312431386674/1330884061144551427/1330986250642919556 could you first explain how this would be possible?
other-emerald
other-emerald•8mo ago
where is this pointing to?
optimistic-gold
optimistic-goldOP•8mo 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...
other-emerald
other-emerald•8mo ago
use TanStack query for this as a cache
optimistic-gold
optimistic-goldOP•8mo ago
what would the pseudo code in the __root beforeLoad look like? That's where I'm confused If going with TSQ^
other-emerald
other-emerald•8mo ago
const data = await queryClient.ensureQueryData(opts)
if(!data.user) {
throw redirect(...)
}
const data = await queryClient.ensureQueryData(opts)
if(!data.user) {
throw redirect(...)
}
optimistic-gold
optimistic-goldOP•8mo ago
I am also on v4 if that matters
other-emerald
other-emerald•8mo ago
don't think you need more than that
optimistic-gold
optimistic-goldOP•8mo ago
dont think ensureQueryData is in v4 😓
other-emerald
other-emerald•8mo 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:...
other-emerald
other-emerald•8mo ago
it is
optimistic-gold
optimistic-goldOP•8mo ago
ok got it! thank you! Will work with this
vicious-gold
vicious-gold•8mo ago
I also want to mention that this should be in _authenticated/, you dont need anything in __root
other-emerald
other-emerald•8mo ago
absolutely right
vicious-gold
vicious-gold•8mo ago
unless you want to pass the user to the context of routes outside of _authenticated/
other-emerald
other-emerald•8mo ago
otherwise you cant login
optimistic-gold
optimistic-goldOP•8mo ago
right! And for logout, would the queryCache invalidation be enough?
other-emerald
other-emerald•8mo ago
you would also need to router.invalidate()
optimistic-gold
optimistic-goldOP•8mo ago
Or is the router.invalidate needed? right
other-emerald
other-emerald•8mo ago
to force reexecution of beforeLoad
optimistic-gold
optimistic-goldOP•8mo ago
sweet
other-emerald
other-emerald•8mo ago
and thus redirecting
optimistic-gold
optimistic-goldOP•8mo ago
🤞 Quick question - the queryClient should be passed into the router context I'm guessing? To be accessible in beforeLoad?
other-emerald
other-emerald•8mo ago
can be could also just be imported only matters if you want to ever inject another instance e.g. during testing
optimistic-gold
optimistic-goldOP•8mo ago
I can't call const queryClient = useQueryClient(); in the beforeLoad right? How would I import it?
other-emerald
other-emerald•8mo ago
just import it 🤪 if you have export queryClient = ... somewhere you would just import that instance but using router context is much cleaner
optimistic-gold
optimistic-goldOP•8mo 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?
other-emerald
other-emerald•8mo ago
yes and this is also how we do it in the react query based router examples
optimistic-gold
optimistic-goldOP•8mo 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
other-emerald
other-emerald•8mo ago
would need a minimal complete example can you fork an existing one from stackblitz and modify accordingly ?
optimistic-gold
optimistic-goldOP•8mo 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
optimistic-gold
optimistic-goldOP•8mo ago
No description
optimistic-gold
optimistic-goldOP•8mo 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
vicious-gold
vicious-gold•8mo ago
are you using axios by chance?
optimistic-gold
optimistic-goldOP•8mo ago
yes iam
vicious-gold
vicious-gold•8mo 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?
optimistic-gold
optimistic-goldOP•8mo ago
Ohh
vicious-gold
vicious-gold•8mo ago
try {
// ...
} catch (error) {
if (error.status === 403) throw redirect(/* ... */)

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

throw error
}
optimistic-gold
optimistic-goldOP•8mo ago
yes correct But in the _unauthenticated case where there isn't an error, I want to redirect to /
vicious-gold
vicious-gold•8mo 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
optimistic-gold
optimistic-goldOP•8mo 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 /
vicious-gold
vicious-gold•8mo ago
// _unauthenticated/
beforeLoad: ({ context }) => {
if (context.user) throw redirect(...) // To '/'
}
// _unauthenticated/
beforeLoad: ({ context }) => {
if (context.user) throw redirect(...) // To '/'
}
optimistic-gold
optimistic-goldOP•8mo ago
I see what you're doing! You're decoupling the beforeLoads.. And just __root handles the axios GET
vicious-gold
vicious-gold•8mo 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
optimistic-gold
optimistic-goldOP•8mo ago
Ya makes sense Just one question and again this might be stupid
vicious-gold
vicious-gold•8mo ago
but, again, make the request using tanstack query, to cache the user and avoid other requests for it np, ask away
optimistic-gold
optimistic-goldOP•8mo 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?
vicious-gold
vicious-gold•8mo ago
it's going to show as User | null the type wont change
optimistic-gold
optimistic-goldOP•8mo ago
yes but how does it infer the type there? I'm not defining the type explicitly anywhere right?
vicious-gold
vicious-gold•8mo ago
it doesnt? you need to specify it when you pass it to createRootRouteWithContext
optimistic-gold
optimistic-goldOP•8mo ago
Ah I see
vicious-gold
vicious-gold•8mo ago
thats why I told you to type it as User | null, you give the type for it
optimistic-gold
optimistic-goldOP•8mo ago
Makes sense
vicious-gold
vicious-gold•8mo ago
nothing gets inferred
optimistic-gold
optimistic-goldOP•8mo ago
Perfect. All of it makes sense now I'm curious - does this setup seem wrong? Or inefficient
vicious-gold
vicious-gold•8mo ago
not at all, this is pretty much the recommended way
optimistic-gold
optimistic-goldOP•8mo ago
Even how I have my apis set up? where I do a /user to check for login or not
vicious-gold
vicious-gold•8mo 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
optimistic-gold
optimistic-goldOP•8mo 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
vicious-gold
vicious-gold•8mo ago
Gl

Did you find this page helpful?