T
TanStack5d ago
statutory-emerald

Need Help! Child Dynamic Route Loader affects Parent Route Query?

I've the following setup: Using Supabase for auth/db stuff, i have defined both a client and a server instance in __root.tsx I call supabase auth getUser and getSession, pass it down to the component which uses useEffect to pass the session to the supabase client instance _protected.tsx beforeLoad checks if getUser() returns a user, redirects to /login otherwise /_protected/leads/index.tsx renders a table of leads, using a hook within the component that returns a useQuery which calls supabase (client instance) gets leads returns data /_protected/leads/$leadsId/route.tsx detailed view of leads, router loader checks if data exists in query cache, if not calls database, returns data for the component to render. Then component has another hook within for mutations etc My understanding of tanstack start is that I can have both server side rendering and client side rendering. In /leads im doing things client side and in /leads/id im using the loader which is server side. however what im really struggling to understand is that when I'm calling the hook from /leads, its stuck pending/fetching/loading without ever returning data or an error IF the loader in /leads/id is present. If I remove the loader the query runs fine? Can someone help me please, I've googled anywhere and no LLM has any idea whats going on either, so I've done my research. Going through the docs one more time in the meantime but this really feels strange. Am I forced to not use the loader function in /leads/id? If that's the case its fine I just want to know why this mismatch happens My implementation is quite barebones so I dont believe there's an issue with the query hooks
4 Replies
sensitive-blue
sensitive-blue5d ago
can you please provide a complete example repo to that reproduces this?
statutory-emerald
statutory-emeraldOP5d ago
that would take a bit but I believe it would be much easier to just show the code here, its really just starter stuff.
structure

routes/
├── _protected/
│ └── leads/
│ ├── index.tsx
│ └── $leadId/
│ └── route.tsx
├── _protected.tsx
└── _root.tsx
queries/
└── leads.ts
libs/
├── client.ts //supabase browser client
└── server.ts //supabase server client
structure

routes/
├── _protected/
│ └── leads/
│ ├── index.tsx
│ └── $leadId/
│ └── route.tsx
├── _protected.tsx
└── _root.tsx
queries/
└── leads.ts
libs/
├── client.ts //supabase browser client
└── server.ts //supabase server client
/queries/leads.ts

import {supabase} from "@/libs/client.ts"

export const leadsQueryKeys = {
leadsWithOwners: () => ["leads", "withOwners"] as const,
lead: (id: number) => ['lead', id] as const,
}

// Query function to fetch all leads
export async function getLeads() {
const { data, error } = await supabase.from("lead").select(`*, user (id, name, email)`);

if (error) {
console.log(error.message)
throw new Error(error.message);
}

return data;
}

// Hook to use in components
export const useGetLeadsWithOwners = () => {
return useQuery<Array<LeadWithOwner>, Error>({
queryKey: leadsQueryKeys.leadsWithOwners(),
queryFn: getLeads,
});
};
/queries/leads.ts

import {supabase} from "@/libs/client.ts"

export const leadsQueryKeys = {
leadsWithOwners: () => ["leads", "withOwners"] as const,
lead: (id: number) => ['lead', id] as const,
}

// Query function to fetch all leads
export async function getLeads() {
const { data, error } = await supabase.from("lead").select(`*, user (id, name, email)`);

if (error) {
console.log(error.message)
throw new Error(error.message);
}

return data;
}

// Hook to use in components
export const useGetLeadsWithOwners = () => {
return useQuery<Array<LeadWithOwner>, Error>({
queryKey: leadsQueryKeys.leadsWithOwners(),
queryFn: getLeads,
});
};
_protected.tsx

export const Route = createFileRoute('/_protected')({
beforeLoad: async () => {
const user = await fetchUser() // serverFn that calls supabase server getUser and returns user

if (!user) {
throw redirect({ to: '/login' })
}

return {
user,
}
},
})
_protected.tsx

export const Route = createFileRoute('/_protected')({
beforeLoad: async () => {
const user = await fetchUser() // serverFn that calls supabase server getUser and returns user

if (!user) {
throw redirect({ to: '/login' })
}

return {
user,
}
},
})
_root.tsx

const fetchUserSession = createServerFn({ method: 'GET' }).handler(async () => {
const supabase = getSupabaseServerClient()
const { data: { session }, error } = await supabase.auth.getSession()
const { data: user } = await supabase.auth.getUser()
return {
session: session ?? null,
user: user ?? null
}
})


interface MyRouterContext {
queryClient: QueryClient,
// supabaseServer: ReturnType<typeof getSupabaseServerClient>
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
beforeLoad: async () => {
const { session, user } = await fetchUserSession()

return {
session,
user
}
},
....

function RootDocument({ children }: { children: React.ReactNode }) {
const { session, user } = Route.useRouteContext()
const supabase = getSupabaseClient()

// hydrate the client session (only runs once)
useEffect(() => {
if (session) {
supabase.auth.setSession({
access_token: session?.access_token,
refresh_token: session?.refresh_token,
})
}
}, [session])
...

return children
_root.tsx

const fetchUserSession = createServerFn({ method: 'GET' }).handler(async () => {
const supabase = getSupabaseServerClient()
const { data: { session }, error } = await supabase.auth.getSession()
const { data: user } = await supabase.auth.getUser()
return {
session: session ?? null,
user: user ?? null
}
})


interface MyRouterContext {
queryClient: QueryClient,
// supabaseServer: ReturnType<typeof getSupabaseServerClient>
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
beforeLoad: async () => {
const { session, user } = await fetchUserSession()

return {
session,
user
}
},
....

function RootDocument({ children }: { children: React.ReactNode }) {
const { session, user } = Route.useRouteContext()
const supabase = getSupabaseClient()

// hydrate the client session (only runs once)
useEffect(() => {
if (session) {
supabase.auth.setSession({
access_token: session?.access_token,
refresh_token: session?.refresh_token,
})
}
}, [session])
...

return children
/_protected/leads/index.tsx


export const Route = createFileRoute('/_protected/leads/')({
component: RouteComponent
})

function RouteComponent() {
...
const {
data,
isFetching,
isPending,
isLoading,
isLoadingError,
isError,
error,
refetch
} = useGetLeadsWithOwners();

...

if(error) return <>error<>

return (<table data={data}/>}
}
/_protected/leads/index.tsx


export const Route = createFileRoute('/_protected/leads/')({
component: RouteComponent
})

function RouteComponent() {
...
const {
data,
isFetching,
isPending,
isLoading,
isLoadingError,
isError,
error,
refetch
} = useGetLeadsWithOwners();

...

if(error) return <>error<>

return (<table data={data}/>}
}
_protected/leads/$leadId/route.tsx



export const Route = createFileRoute('/_protected/leads/$leadId')({
loader: async ({ params, context }) => {
const { queryClient } = context
console.error(params.leadId)

const supabase = getSupabase()

// const leadId = Number(params.leadId.split("_")[1])
const leadId = Number(params.leadId)

console.log(leadId)

if (isNaN(leadId)) {
throw new Error("Invalid lead ID")
}

const cachedLeads = queryClient.getQueryData<Array<LeadWithOwner>>(
leadsQueryKeys.leadsWithOwners()
)

const cachedLead = cachedLeads?.find((lead) => lead.id === leadId)

if (cachedLead) {
// ✅ Seed the individual lead query with cached data
queryClient.setQueryData(leadsQueryKeys.lead(leadId), cachedLead)
return { lead: cachedLead }
}

console.log("Something is happening");
// ❌ Fallback: Fetch from network if not in list cache
try {
const { data: userData, error: userError } = await supabase.auth.getUser()

if (!userData) {
throw new Error(userError?.message);
}


// const { data, error } = await useGetLeadDetails(leadId)
const { data, error } = await supabase.from("lead").select("*, user (id, name, email)").eq('id', leadId).single();

if (error) {
throw new Error(error.message);
}

queryClient.setQueryData(leadsQueryKeys.lead(leadId), data)

return { lead: data };

} catch (error) {
console.error(error)
// throw notFound()
}

},
component: RouteComponent,
errorComponent: () => (<h1>Error</h1>)
})
_protected/leads/$leadId/route.tsx



export const Route = createFileRoute('/_protected/leads/$leadId')({
loader: async ({ params, context }) => {
const { queryClient } = context
console.error(params.leadId)

const supabase = getSupabase()

// const leadId = Number(params.leadId.split("_")[1])
const leadId = Number(params.leadId)

console.log(leadId)

if (isNaN(leadId)) {
throw new Error("Invalid lead ID")
}

const cachedLeads = queryClient.getQueryData<Array<LeadWithOwner>>(
leadsQueryKeys.leadsWithOwners()
)

const cachedLead = cachedLeads?.find((lead) => lead.id === leadId)

if (cachedLead) {
// ✅ Seed the individual lead query with cached data
queryClient.setQueryData(leadsQueryKeys.lead(leadId), cachedLead)
return { lead: cachedLead }
}

console.log("Something is happening");
// ❌ Fallback: Fetch from network if not in list cache
try {
const { data: userData, error: userError } = await supabase.auth.getUser()

if (!userData) {
throw new Error(userError?.message);
}


// const { data, error } = await useGetLeadDetails(leadId)
const { data, error } = await supabase.from("lead").select("*, user (id, name, email)").eq('id', leadId).single();

if (error) {
throw new Error(error.message);
}

queryClient.setQueryData(leadsQueryKeys.lead(leadId), data)

return { lead: data };

} catch (error) {
console.error(error)
// throw notFound()
}

},
component: RouteComponent,
errorComponent: () => (<h1>Error</h1>)
})
so in /leads when there's a loader function in /leads/id the query simply stalls. No 404, no nothing. It doesn't seem to reach the DB at all, because at first I thought it was a server side calling supabase browser client issue. But that doesnt seem to be it either When I remove the loader, it runs fine. I don't think it has anything to do with the contents of the loader function as even if i just return null in it and do nothing the same behavior happens this happens even if i split the routes into siblings, so lead/1 and leads for the big table of leads. the loader in lead/id messes up the rendering of leads
sensitive-blue
sensitive-blue5d ago
really, a complete example would help here to debug
statutory-emerald
statutory-emeraldOP5d ago
Nvm, i got it. Calling supabase client from within the loader seems to have caused the issue. Passing it as context to the loader resolves it. Noob mistake

Did you find this page helpful?