T
TanStack6mo ago
like-gold

Is there a more idiomatic way to do this? Getting Auth Data from Server and Client

So, I am using better auth and Start. I want to eliminate any flicker. that means checking auth status in loaders, but if the user signs out (or signs in) since this is also a SPA, we should use client data as well. That lead me to do the following in routes/__root.tsx:
export const authStateFn = createServerFn({method: "GET"}).handler(async () => {
const {headers} = getEvent()
const data = await auth.api.getSession({headers})
return data
})

export const Route = createRootRoute({
beforeLoad: async () => { return await authStateFn() },
loader: async ({context}) => {
if (!context?.session || !context?.user) return null
return {
session: context.session,
user: context.user
}
},
staleTime: 60_000,
component: RootComponent,
})
export const authStateFn = createServerFn({method: "GET"}).handler(async () => {
const {headers} = getEvent()
const data = await auth.api.getSession({headers})
return data
})

export const Route = createRootRoute({
beforeLoad: async () => { return await authStateFn() },
loader: async ({context}) => {
if (!context?.session || !context?.user) return null
return {
session: context.session,
user: context.user
}
},
staleTime: 60_000,
component: RootComponent,
})
This gets the data into the root loader. since I check auth status on every page (at the very least, to know whether or not to show the sign in or sign out buttons), I set the staleTime to 60 seconds to slow down unnecesary requests to the database Then I created a custom hook
const isServer = typeof window === "undefined"

export function useAuthData() {
const {data: clientData, isPending} = useSession()
const rootRouteApi = getRouteApi("__root__")
const serverData = rootRouteApi.useLoaderData()
const serverAuthData = serverData?.session && serverData?.user ? {session: serverData.session, user: serverData.user} : null

return (isPending || isServer) ? serverAuthData : clientData
}
const isServer = typeof window === "undefined"

export function useAuthData() {
const {data: clientData, isPending} = useSession()
const rootRouteApi = getRouteApi("__root__")
const serverData = rootRouteApi.useLoaderData()
const serverAuthData = serverData?.session && serverData?.user ? {session: serverData.session, user: serverData.user} : null

return (isPending || isServer) ? serverAuthData : clientData
}
isPending is annoyingly false on servers, so I have to check if we are in a server context or pending on client data. if that is the case, I use the server data from the root loader above. otherwise, we can use clientData now I have a flicker free ui, but is there a more idiomatic way to do this?
5 Replies
yelping-magenta
yelping-magenta6mo ago
You are already putting the user/session in the context by returning them from root’s beforeLoad. So you can read user/session anywhere using just Route.useRouteContext() (will be typesafe automatically) You could also, instead of checking auth in every page do it in a layout beforeLoad and redirect to a login page if there is no session in the context
like-gold
like-goldOP6mo ago
Oh, useRouteContext looks nicer! I am using auth state to determine if I render sign in or sign out buttons, which are on a Nav as a child of the RootComponent, which is why I'm doing it there I guess then I could skip the loader entirely and just use beforeLoad... but then I'll need to implement my own caching 🤔
eastern-cyan
eastern-cyan6mo ago
cache using query?
yelping-magenta
yelping-magenta6mo ago
Yes, I also suggest to consider using tanstack query as you’ll probably end up build something very similar otherwise. Here you can se an example using better-auth and tanstack query https://github.com/dotnize/tanstarter/blob/main/src/routes/__root.tsx
GitHub
tanstarter/src/routes/__root.tsx at main · dotnize/tanstarter
minimal TanStack Start template with Better Auth, Drizzle ORM, shadcn/ui - dotnize/tanstarter
like-gold
like-goldOP6mo ago
oh, some really good patterns in here, thanks!

Did you find this page helpful?