mutation taking a long time to appear after prefetching query

Hello, I'm am currently having an issue where if my isCaptchaValid endpoint errors (using throw new TRPCError(...)) the error message takes a long time to come through, it appears that it is linked to the endpoints execution time, as if I add some code to wait 2 seconds before executing it dramatically increases the time it takes for the error to come back. I am using nextjs loading.tsx which is how the loading animation works, so as soon as the loading animation is gone it means that TRPC data has finished fetching, so the error message should be appearing instantly? In the below server component you can see I am using prefetch and in the client component you can see I am using useQuery, which should not be refetching on first render since I used prefetch function. Server component:
export default async function Page({ params }: Props) {
const { guildId, uuid } = await params;
const session = await auth();
await api.verification.isCaptchaValid.prefetch({ guildId, uuid });
if (!session?.user.accountProviderId) redirect(`/login?callbackUrl=/verify/${guildId}/${uuid}`);

return (
<HydrateClient>
<div className="container flex flex-1 items-center justify-center">
<Content guildId={guildId} uuid={uuid} />
</div>
</HydrateClient>
);
}
export default async function Page({ params }: Props) {
const { guildId, uuid } = await params;
const session = await auth();
await api.verification.isCaptchaValid.prefetch({ guildId, uuid });
if (!session?.user.accountProviderId) redirect(`/login?callbackUrl=/verify/${guildId}/${uuid}`);

return (
<HydrateClient>
<div className="container flex flex-1 items-center justify-center">
<Content guildId={guildId} uuid={uuid} />
</div>
</HydrateClient>
);
}
client component:
export const Content = ({ guildId, uuid }: Props) => {
const { data: isCaptchaValid, error: invalidCaptchaError } = api.verification.isCaptchaValid.useQuery({
guildId,
uuid,
});

if (!isCaptchaValid)
return (
<div className="flex items-center gap-5">
<UserXIcon className="h-12 w-12 text-destructive" />
<div>
<h1 className="text-2xl font-semibold">Invalid Captcha</h1>
<p>{invalidCaptchaError?.message}</p>
</div>
</div>
);

return <h1>Success</h1>
};
export const Content = ({ guildId, uuid }: Props) => {
const { data: isCaptchaValid, error: invalidCaptchaError } = api.verification.isCaptchaValid.useQuery({
guildId,
uuid,
});

if (!isCaptchaValid)
return (
<div className="flex items-center gap-5">
<UserXIcon className="h-12 w-12 text-destructive" />
<div>
<h1 className="text-2xl font-semibold">Invalid Captcha</h1>
<p>{invalidCaptchaError?.message}</p>
</div>
</div>
);

return <h1>Success</h1>
};
5 Replies
Peform
PeformOP17h ago
im just so confused, because the query knows it errored, but <hydrateclient> isn't passing the error object down to the client still having alot of problems with this, everything I try just results in the same issue, or some incredibly bad code. I tried fetching inside of ssr component (without using prefetch) but parsing errors (trhow trpcError()) inside of a ssr component seems to be incredibly tedius, having to wrap try/catch and parse each error it could be.
Mocha
Mocha17h ago
const [isCaptchaValid] = api.verification.isCaptchaValid.useSuspenseQuery({
guildId,
uuid,
})
const [isCaptchaValid] = api.verification.isCaptchaValid.useSuspenseQuery({
guildId,
uuid,
})
There are 2 ways you can use prefetch. First, call prefetch from the parent (must be server).
'use server'

import { Suspense } from 'react'
import { api, HydrateClient } from '~/trpc/server'

export async function Parent() {
void api.user.team.prefetch()

return (
<HydrateClient>
<Suspense fallback={<div>Loading...</div>}>
<Child />
</Suspense>
</HydrateClient>
)
}
'use server'

import { Suspense } from 'react'
import { api, HydrateClient } from '~/trpc/server'

export async function Parent() {
void api.user.team.prefetch()

return (
<HydrateClient>
<Suspense fallback={<div>Loading...</div>}>
<Child />
</Suspense>
</HydrateClient>
)
}
Then use the query from either a client or server component.
'use server'

import { api } from '~/trpc/server'

export async function Child() {
const team = await api.user.team()

return <div>{team.name}</div>
}
'use server'

import { api } from '~/trpc/server'

export async function Child() {
const team = await api.user.team()

return <div>{team.name}</div>
}
or
'use client'

import { api } from "~/trpc/react"

export function Child() {
const [team] = api.user.team.useSuspenseQuery()

return <div>{team.name}</div>
}
'use client'

import { api } from "~/trpc/react"

export function Child() {
const [team] = api.user.team.useSuspenseQuery()

return <div>{team.name}</div>
}
Notice how there's no error or isLoading with useSuspenseQuery because both are handled by the parent's Suspense Handle loading with fallback='Loading...' or with a loading.tsx file Handle errors with error.tsx files Also handle if (...) redirect() before prefetch() not after it
Peform
PeformOP16h ago
Hi, thank you for the response. I am using loading.tsx files for global loading page, which is working well, so I have not used <Suspense/>. Before you sent this updated message I was trying to do the below code, but unfortunately it just causes the frontend to crash because the query threw an error. A query that was dehydrated as pending ended up rejecting It looks like you can access the query states in the data variable, which includes the "error" but the page crashes before i can use this. I'm not sure im a big fan of using error.tsx pages, but i can give that a go if its not possible to do something like the below:
export const Content = ({ guildId, uuid, session }: Props) => {
const [isCaptchaValid, data] = api.verification.isCaptchaValid.useSuspenseQuery({
guildId,
uuid,
});

if (data.error) {
return <InvalidCaptcha session={session} error={data.error?.message ?? 'Invlaid captcha'} showLoggedInAs />;
}

return <h1>success</h1>;
};
export const Content = ({ guildId, uuid, session }: Props) => {
const [isCaptchaValid, data] = api.verification.isCaptchaValid.useSuspenseQuery({
guildId,
uuid,
});

if (data.error) {
return <InvalidCaptcha session={session} error={data.error?.message ?? 'Invlaid captcha'} showLoggedInAs />;
}

return <h1>success</h1>;
};
server:
export default async function Page({ params }: Props) {
const { guildId, uuid } = await params;
const session = await auth();
if (!session) redirect(`/login?callbackUrl=/verify/${guildId}/${uuid}`);

await api.verification.isCaptchaValid.prefetch({ guildId, uuid });

return (
<HydrateClient>
<div className="flex flex-1 items-center justify-center lg:container">
<Content guildId={guildId} uuid={uuid} session={session} />
</div>
</HydrateClient>
);
}
export default async function Page({ params }: Props) {
const { guildId, uuid } = await params;
const session = await auth();
if (!session) redirect(`/login?callbackUrl=/verify/${guildId}/${uuid}`);

await api.verification.isCaptchaValid.prefetch({ guildId, uuid });

return (
<HydrateClient>
<div className="flex flex-1 items-center justify-center lg:container">
<Content guildId={guildId} uuid={uuid} session={session} />
</div>
</HydrateClient>
);
}
If I do use the error.tsx method is typescript able to obtain the trpc error type? I have a bunch of different errors that can happen on various endpoints and I set custom error messages which are used to tell the user what went wrong, but some error pages require more data than others, which is why I want to handle this on the frontend using the above method as I can know exactly which query errored and customize how the error is viewed as needed
Mocha
Mocha14h ago
Yes, data.error won't behave like you expect it to. Using error.tsx will allow you to show an error message.
// ./error.tsx
'use client'

export default function Error({ error }: { error: Error }) {
return <p>Error: {error.message}</p>
}
// ./error.tsx
'use client'

export default function Error({ error }: { error: Error }) {
return <p>Error: {error.message}</p>
}
What I like to do instead is to just return the error as a value. This gets rid of many unexpected behaviors.
'use server'

export default async function Content({ guildId, uuid, session }: Props) {
const isCaptchaValid = await api.verification.isCaptchaValid({
guildId,
uuid,
})

if (isCaptchaValid.error) {
return (
<InvalidCaptcha
session={session}
error={isCaptchaValid.error.message ?? 'Invlaid captcha'}
showLoggedInAs
/>
)
}

return <h1>success</h1>
}
'use server'

export default async function Content({ guildId, uuid, session }: Props) {
const isCaptchaValid = await api.verification.isCaptchaValid({
guildId,
uuid,
})

if (isCaptchaValid.error) {
return (
<InvalidCaptcha
session={session}
error={isCaptchaValid.error.message ?? 'Invlaid captcha'}
showLoggedInAs
/>
)
}

return <h1>success</h1>
}
Also if your Content component really looks like that, you don't need it to be a Client Component.

Did you find this page helpful?