How are you supposed to handle loading states for trpc (useQuery) calls in client comps in Next?

I have a client component that calls for data via trpc. I extract isLoading from those useQuery calls and then I want to display a loading skeleton while the data is loading. However, when I follow the normal React render pattern (check isLoading -> render skeleton; check if data empty -> render null; otherwise render content), I get an error about hydration failing because inital UI does not match server-rendered UI. I think this is because the isLoading state is always false on the server. This isn't an issue in standard React (non-RSC), because you make your useQuery call before trying to render anything, so isLoading is already true before you reach that render pattern logic. What's the right way to structure this in Next so I can show my loading state without a hydration error? Here's the code:
"use client"

// imports

export function ActiveArtists() {
const { getValue: getDeviceId } = useLocalStorage({
key: DEVICE_ID_LOCAL_STORAGE_KEY,
defaultValue: "",
});

const { data: activeGrid, isLoading: isActiveGridLoading } =
api.grids.getActiveGrid.useQuery(
{ device_id: getDeviceId() },
{ enabled: !!getDeviceId() },
);

const { data: artists, isLoading: isArtistsLoading } =
api.grids.getArtists.useQuery(
{ grid_id: Number(activeGrid?.id) ?? 0 },
{ enabled: !!activeGrid?.id },
);

if (isActiveGridLoading || isArtistsLoading) {
return (
<div className="flex items-center space-x-4">
<Skeleton className={cn(avatarClasses.lg, "rounded-full")} />
<Skeleton className={cn(avatarClasses.lg, "rounded-full")} />
<Skeleton className={cn(avatarClasses.lg, "rounded-full")} />
</div>
);
}

if (!activeGrid?.id || !artists?.length) {
return null;
}

const validArtists = artists.filter(
(artist) => artist?.id && artist?.username,
);

return <ArtistList artists={validArtists} />;
}
"use client"

// imports

export function ActiveArtists() {
const { getValue: getDeviceId } = useLocalStorage({
key: DEVICE_ID_LOCAL_STORAGE_KEY,
defaultValue: "",
});

const { data: activeGrid, isLoading: isActiveGridLoading } =
api.grids.getActiveGrid.useQuery(
{ device_id: getDeviceId() },
{ enabled: !!getDeviceId() },
);

const { data: artists, isLoading: isArtistsLoading } =
api.grids.getArtists.useQuery(
{ grid_id: Number(activeGrid?.id) ?? 0 },
{ enabled: !!activeGrid?.id },
);

if (isActiveGridLoading || isArtistsLoading) {
return (
<div className="flex items-center space-x-4">
<Skeleton className={cn(avatarClasses.lg, "rounded-full")} />
<Skeleton className={cn(avatarClasses.lg, "rounded-full")} />
<Skeleton className={cn(avatarClasses.lg, "rounded-full")} />
</div>
);
}

if (!activeGrid?.id || !artists?.length) {
return null;
}

const validArtists = artists.filter(
(artist) => artist?.id && artist?.username,
);

return <ArtistList artists={validArtists} />;
}
0 Replies
No replies yetBe the first to reply to this messageJoin

Did you find this page helpful?