T
TanStack2mo ago
extended-salmon

How do I get context set in root route?

interface RouterContext {
player?: ReturnType<typeof usePlayer>
}

export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent,
})

function RootComponent() {
// I want context here
return (
<>
{/* <TopBar /> */}
<SidebarProvider>
<AppSidebar />
<main>
<Outlet />
</main>
</SidebarProvider>
<TanStackRouterDevtools position='bottom-right' />
</>
)
}
interface RouterContext {
player?: ReturnType<typeof usePlayer>
}

export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent,
})

function RootComponent() {
// I want context here
return (
<>
{/* <TopBar /> */}
<SidebarProvider>
<AppSidebar />
<main>
<Outlet />
</main>
</SidebarProvider>
<TanStackRouterDevtools position='bottom-right' />
</>
)
}
I am setting context per the docs that fetches but I can't find any documentation on how to actually use this context in a component. I see that you can use it in load and beforeLoad, but nothing about passing it or using it in the route's component. Am I thinking of it as more of a state management solution than it is?
14 Replies
correct-apricot
correct-apricot2mo ago
Route.useRouteContext()
extended-salmon
extended-salmonOP2mo ago
I tried that and I'm getting this returned:
{
"status": "pending",
"fetchStatus": "idle",
"isPending": true,
"isSuccess": false,
"isError": false,
"isInitialLoading": false,
"isLoading": false,
"dataUpdatedAt": 0,
"error": null,
"errorUpdatedAt": 0,
"failureCount": 0,
"failureReason": null,
"errorUpdateCount": 0,
"isFetched": false,
"isFetchedAfterMount": false,
"isFetching": false,
"isRefetching": false,
"isLoadingError": false,
"isPaused": false,
"isPlaceholderData": false,
"isRefetchError": false,
"isStale": false,
"promise": {
"status": "rejected",
"reason": {}
}
}
{
"status": "pending",
"fetchStatus": "idle",
"isPending": true,
"isSuccess": false,
"isError": false,
"isInitialLoading": false,
"isLoading": false,
"dataUpdatedAt": 0,
"error": null,
"errorUpdatedAt": 0,
"failureCount": 0,
"failureReason": null,
"errorUpdateCount": 0,
"isFetched": false,
"isFetchedAfterMount": false,
"isFetching": false,
"isRefetching": false,
"isLoadingError": false,
"isPaused": false,
"isPlaceholderData": false,
"isRefetchError": false,
"isStale": false,
"promise": {
"status": "rejected",
"reason": {}
}
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent
})

function RootComponent() {
const { player } = Route.useRouteContext()
console.log('player', player)
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent
})

function RootComponent() {
const { player } = Route.useRouteContext()
console.log('player', player)
Here's the hook:
export function usePlayer() {
const { user, isLoaded } = useUser()

return useQuery<Player>({
queryKey: ['player', user?.id],
enabled: isLoaded && !!user,
queryFn: async (): Promise<Player> => {
if (!user) {
throw new Error('User is not available')
}
// TODO: change this to env var
const response = await fetch('http://localhost:3000/api/players/auth', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
clerkUserId: user.id,
email: user.primaryEmailAddress?.emailAddress,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
}),
})

const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to fetch player')
}
console.log('play data:', data.player)
return data.player as Player
},
})
}
export function usePlayer() {
const { user, isLoaded } = useUser()

return useQuery<Player>({
queryKey: ['player', user?.id],
enabled: isLoaded && !!user,
queryFn: async (): Promise<Player> => {
if (!user) {
throw new Error('User is not available')
}
// TODO: change this to env var
const response = await fetch('http://localhost:3000/api/players/auth', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
clerkUserId: user.id,
email: user.primaryEmailAddress?.emailAddress,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
}),
})

const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to fetch player')
}
console.log('play data:', data.player)
return data.player as Player
},
})
}
I don't know what I'm doing wrong, but the hook correctly logs data. My mental model is I add that as the context in the root route and all the child routes can access that data returned from the query via context so it's sort of like state management. Is this wrong? Okay, so I figured out if I hot reload it will update the context data with "success". Is there a way to make the router aware that context has changed without adding a key or something to the context component?
correct-apricot
correct-apricot2mo ago
can you please provide a complete example repo?
extended-salmon
extended-salmonOP2mo ago
I did some more research and I think my mental model was wrong. I was trying to use context as a global state management solution instead of using react query like it’s suggested.
extended-salmon
extended-salmonOP2mo ago
Okay, I actually am having some kind of issue with Auth that directly relates to the above problem. https://github.com/tunztunztunz/tanstack-router-issue Here is a repo that recreates it.
GitHub
GitHub - tunztunztunz/tanstack-router-issue
Contribute to tunztunztunz/tanstack-router-issue development by creating an account on GitHub.
extended-salmon
extended-salmonOP2mo ago
I can't figure out if it's a Router, Query, or Clerk issue or a mixture. In the create file route, i'm logging the context and it says it's not loaded, but then the user does load in and the context isn't updated. The only way I've been able to work around is to use a useEffect in the App.tsx component or set a key that is created by the piece of context I'm passing into the RouterProvider. I simulated the query hooks, so the actualy index.ts looks like this:
export const Route = createFileRoute('/')({
beforeLoad: async ({ context }) => {
console.log('context here', context)
},
component: RouteComponent,
})

function RouteComponent() {
const gladiators = useGladiators()

return (
<div>
{/* Show gladiator overview if we have gladiators, otherwise show create component */}
{gladiators?.data && gladiators.data.length > 0 ? (
<GladiatorOverviewCard />
) : (
<CreateFirstGladiator />
)}
</div>
)
}
export const Route = createFileRoute('/')({
beforeLoad: async ({ context }) => {
console.log('context here', context)
},
component: RouteComponent,
})

function RouteComponent() {
const gladiators = useGladiators()

return (
<div>
{/* Show gladiator overview if we have gladiators, otherwise show create component */}
{gladiators?.data && gladiators.data.length > 0 ? (
<GladiatorOverviewCard />
) : (
<CreateFirstGladiator />
)}
</div>
)
}
correct-apricot
correct-apricot2mo ago
how does logging in work here? or what does "user does load in" mean?
extended-salmon
extended-salmonOP2mo ago
When a user isn't authenticated they click the sign in button, go through Clerk auth, then get redirected back to the page. Clerk has isLoaded and user and the query is enabled if they are both truthy. The routes will always flash a logged out state before a logged in state. I think there's an issue with how the auth is being handled?
correct-apricot
correct-apricot2mo ago
@wobsoriano mabe you can help out here?
ambitious-aqua
ambitious-aqua2mo ago
This is not an issue It's completely normal and that's how Clerk works useAuth will always return undefined first and then load the user in. You'll need to handle that. If you want that to not happen you're probably looking at an SSR solution with Clerk vs. using it client side user = undefined means its loading user = false means the user is logged out user = true means the user is logged in But it always starts as undefined
extended-salmon
extended-salmonOP2mo ago
How come when I console.log out my context it shows false but the user is logged in? I was having a similar issue when using context on the root route as well. Am I thinking about context wrong? I thought if I’m passing it and it changes then that should get picked up and I can access it/use it in components but it seems like it gets set when the app starts and never updates.
adverse-sapphire
adverse-sapphire2mo ago
Hi @dusting! For router apps, I recommend using the @clerk/clerk-react package to take advantage of context. See example here https://github.com/wobsoriano/tanstack-router-clerk
GitHub
GitHub - wobsoriano/tanstack-router-clerk
Contribute to wobsoriano/tanstack-router-clerk development by creating an account on GitHub.
adverse-sapphire
adverse-sapphire2mo ago
The code you might be interested in
function InnerApp() {
const { isLoaded, user } = useUser()

// If the provider is initially loading, do not render the router
if (!isLoaded) {
return (
<div className="flex h-screen w-full items-center justify-center p-4">
<div className="size-10 rounded-full border-4 border-gray-200 border-t-foreground animate-spin" />
</div>
)
}

return <RouterProvider router={router} context={{ user }} />
}
function InnerApp() {
const { isLoaded, user } = useUser()

// If the provider is initially loading, do not render the router
if (!isLoaded) {
return (
<div className="flex h-screen w-full items-center justify-center p-4">
<div className="size-10 rounded-full border-4 border-gray-200 border-t-foreground animate-spin" />
</div>
)
}

return <RouterProvider router={router} context={{ user }} />
}
extended-salmon
extended-salmonOP2mo ago
Finally seeing this. Going to check it out. thank you! So basically, the entirety of the app sort of lives inside of the _auth layout then? If most of your app requires authentication?

Did you find this page helpful?