T
TanStack14mo ago
variable-lime

Routing with async data

Consider the following setup where my context has user: User | null:
// app.tsx

const {data: user} = useUser();

...

<RouterProvider router={router} context={{user}}/>
// app.tsx

const {data: user} = useUser();

...

<RouterProvider router={router} context={{user}}/>
Note that useUser is a react query. On direct navigation to this page I want to check this users permissions list, and redirect them if they don't have it
// protected-page.tsx with route /_authenticated/_sidebarLayout/protected-page/
...
beforeLoad: ({context}) => {
if (!context.user?.permissions.contains(x))
{
redirect
}
}
// protected-page.tsx with route /_authenticated/_sidebarLayout/protected-page/
...
beforeLoad: ({context}) => {
if (!context.user?.permissions.contains(x))
{
redirect
}
}
How can I achieve this, as user is initially null until the data loads from the query - redirecting them every time? Would I have to add some kind of loading into the context and catch that at a higher level perhaps for all pages this might affect?
27 Replies
foreign-sapphire
foreign-sapphire14mo ago
I have the exact same problem
stormy-gold
stormy-gold14mo ago
If your user if in react-query, why not just make sure the user is there and then read the permissions
// user.utils.ts
import { queryOptions } from '@tanstack/react-query'
const userOptions = queryOptions({
queryKey: [/** */],
queryFn: () => void // your function
})

// src/routes/auth.tsx
export const Route = createFileRoute('/_auth')({
beforeLoad: ({ context: { queryClient } }) => {
const user = queryClient.ensureQueryData(userOptions)
if (!user || !user.permisions.contains('foo')) {
throw redirect({ to: '/somewhere' })
}
}
})
// user.utils.ts
import { queryOptions } from '@tanstack/react-query'
const userOptions = queryOptions({
queryKey: [/** */],
queryFn: () => void // your function
})

// src/routes/auth.tsx
export const Route = createFileRoute('/_auth')({
beforeLoad: ({ context: { queryClient } }) => {
const user = queryClient.ensureQueryData(userOptions)
if (!user || !user.permisions.contains('foo')) {
throw redirect({ to: '/somewhere' })
}
}
})
Optionally, whenever the user in React Query changes, you could set up a useEffect to call router.invalidate
foreign-sapphire
foreign-sapphire14mo ago
Hi Sean import { createFileRoute, redirect } from '@tanstack/react-router' import { useRouteContext } from '@tanstack/react-router'; export const Route = createFileRoute('/dashboard')({ component: Dashboard, // Add semicolon here beforeLoad: async ({ context, location }) => { console.log(context.user) if (!context.user) { throw redirect({ to: '/login', search: { redirect: location.href, }, }); } }, }) function Dashboard() { const context = useRouteContext({from: "/dashboard"}); const user = context.user; return ( <div className="p-2"> <h3>Welcome to Dashboard!</h3> User is {"YOLO"} , {user && user.email} <br /> {user?.displayName} </div> ) } this is my component when I refresh this seems to go to redirect always and user also seems null
stormy-gold
stormy-gold14mo ago
You need to first wait for the user call to resolve. Only then should you be calling redirect. You can use either of the solutions I mentioned above. If your user is not in React Query, rather just in react context, then just use the router.invalidate() call, or even just wait till that request has resolved before rendering the <RouterProvider />
stormy-gold
stormy-gold14mo ago
GitHub
nv-rental-clone/src/entry-app.tsx at 508800863fb2936a6e819e5e14b082...
Navotar with Tailwind and the Tanstack. Contribute to SeanCassiere/nv-rental-clone development by creating an account on GitHub.
foreign-sapphire
foreign-sapphire14mo ago
cool, thank you that fixed the issue, I had to wait for the user call to be resolved @ auth things ,
variable-lime
variable-limeOP14mo ago
Cool. thanks for answering that for me. Will give it a go today! Aha, that check in the useEffect sorted the issue for me: if (typeof auth.user === "undefined") return; I'll consider the other solution too
stormy-gold
stormy-gold14mo ago
This is definitely library/implementation specific. It's why we don't prescribe a way of doing auth, since everyone does auth just differently enough that you end up having so many variations 😅.
adverse-sapphire
adverse-sapphire14mo ago
What if I want to show a loader on specific pages instead of all app For example on the following code snippet:
export const createProtectedRoute = ({
component: Component,
path,
redirectTo,
}: ProtectedRouteOptions) => {
return createFileRoute(path)({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: redirectTo ?? "/auth/login" });
}
},
component: Component,
});
};
export const createProtectedRoute = ({
component: Component,
path,
redirectTo,
}: ProtectedRouteOptions) => {
return createFileRoute(path)({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: redirectTo ?? "/auth/login" });
}
},
component: Component,
});
};
stormy-gold
stormy-gold14mo ago
Since you are abstracting away the createRoute function, you'll find this difficult. Normally you'd just set the pendingComponent property when creating a route. You could use a layout route to handle all the auth work and surgically set the pending components. I'd go for a public/authed set of layout routes that have no real UI layer, but rather just used for checking auth on navigate. Something like what I've done here https://github.com/SeanCassiere/nv-rental-clone/tree/master/src/routes I'd stay away from that abstraction on createRoute since that breaks type-safety and makes it a burden in your code-base whenever you want to implement any of the native features.
GitHub
nv-rental-clone/src/routes at master · SeanCassiere/nv-rental-clone
Navotar with Tailwind and the Tanstack. Contribute to SeanCassiere/nv-rental-clone development by creating an account on GitHub.
variable-lime
variable-limeOP13mo ago
I’ll consider the other way too for sure. It seems a nice way to integrate with react query Finally got back round to this again. I'm still having a slight issue as my useUser is actually an authenticated endpoint which relies on isAuthenticated (just checking for tokens)
// app.tsx

const {isAuthenticated} = useAuth();
const {data: user} = useUser(isAuthenticated);

...

<RouterProvider router={router} context={{isAuthenticated, user}}/>
// app.tsx

const {isAuthenticated} = useAuth();
const {data: user} = useUser(isAuthenticated);

...

<RouterProvider router={router} context={{isAuthenticated, user}}/>
useUser has the following:
queryKey: ['user', isAuthenticated],
staleTime: 5 * 60 * 5000, // used on every page so don't really want to make it instant
enabled: isAuthenticated,
queryKey: ['user', isAuthenticated],
staleTime: 5 * 60 * 5000, // used on every page so don't really want to make it instant
enabled: isAuthenticated,
Is it an issue that on logout, my ['user', true] still has the user data in it? Could I just stick a useEffect on isAuthenticated and use removeQueries on the user? Currently on logout I have it set to invalidate the user query, which will be calculated on login again, but it still does one extra call to the useUser endpoint... (and I can see the user data in the cache under ['user', true]
stormy-gold
stormy-gold13mo ago
Yup, or just perform a invalidateQueries call.
variable-lime
variable-limeOP13mo ago
Would the data not remain in the query cache until the user logs in again? Is there any danger in that?
stormy-gold
stormy-gold13mo ago
Thats IMO more of a "its up to decision". Questions like "Realistically, are multiple users sharing the same devices using the same tab instance?" are what you'd need to consider.
variable-lime
variable-limeOP13mo ago
Ah I see. I was considering danger in someone logging out and leaving their pc open or something along those lines Thanks for the response Honestly I'd just like to do a queryClient.reset() on logout or just remove every query key (removeQueries). But as soon as I do that it seems to try to do network calls for all active queries - so I'd have to wait until the user gets to the login page and then call reset, or call reset which triggers every query..? Is removeQueries even supposed to trigger a refetch?
stormy-gold
stormy-gold13mo ago
I suppose it might, if that's query's observer (useQuery) is still active.
variable-lime
variable-limeOP13mo ago
How confusing 🥲 The issue seems to be: - When I logout I set isAuthenticated to false and try to removeQueries - isAuthenticated change triggers a re-render in App - re-render causes hooks to try to fetch again - isAuthenticated causes router to invalidate kicking user to login I don't see a way round this
metropolitan-bronze
metropolitan-bronze13mo ago
can you reproduce this in a minimal complete example? ideally fork one of the router examples on stackblitz
variable-lime
variable-limeOP13mo ago
Sorry can you point me in the direction of those, seems to be quite a few results for that
metropolitan-bronze
metropolitan-bronze13mo ago
metropolitan-bronze
metropolitan-bronze13mo ago
there are lots of examples on the left side
metropolitan-bronze
metropolitan-bronze13mo ago
choose one that fits best
No description
variable-lime
variable-limeOP13mo ago
Ah yeah I remembered seeing it somewhere. Cheers I finally reprod it
variable-lime
variable-limeOP13mo ago
StackBlitz
Router Kitchen Sink React Query File Based Example (forked) - Stack...
Run official live example code for Router Kitchen Sink React Query File Based, created by Tanstack on StackBlitz
variable-lime
variable-limeOP13mo ago
As soon as I add useLocation it does not do what I intend (cache remains uncleared from extra server call) Remove that line, and works as expected To use: Log in, go to “secret” page, press logout. Cache not cleared and extra call to server made. There’s a bunch of faff code as I was trying to repro I mean the clear fix on my local is to not use useLocation Cleaned the code now: https://stackblitz.com/edit/tanstack-router-jnuq6i?file=src%2Froutes%2F_authenticated%2Fsecret.tsx Repro: Log in, go to “secret” page, press logout. Cache not cleared (0 watchers) and extra call to server made. As soon as I add in const location = useLocation() that occurs. I can raise it as an issue.
metropolitan-bronze
metropolitan-bronze13mo ago
when you add const location = useLocation(); to secret.tsx, this component will re-render each time the location changes which will also re-run your hooks
variable-lime
variable-limeOP13mo ago
Hmm I see, so there is no real solution I’ll just not use useLocation then

Did you find this page helpful?