T
TanStack•7mo ago
grumpy-cyan

Client invalidateQueries not triggering refetch for server-side prefetched data

I'm using Next.js 15 with tRPC v11 and TanStack Query. On the server side, I fetch and filter the user list based on query parameters. Here's how the data is prefetched using getQueryClient and hydrated with HydrateClient:
import { getQueryClient, HydrateClient, trpc } from '@/trpc/server';

export default async function Page(props) {
const searchParams = await props.searchParams;
const search = usersSearchParamsCache.parse(searchParams);
const validFilters = getValidFilters(search.filters);
const queryClient = getQueryClient();

const promises = Promise.all([
queryClient.fetchQuery(
trpc.providers.users.list.queryOptions({
...search,
filters: validFilters
})
)
]);

return (
<HydrateClient>
<React.Suspense fallback={<DataTableSkeleton /* props */ />}>
<UsersTable promises={promises} />
</React.Suspense>
</HydrateClient>
);
}
import { getQueryClient, HydrateClient, trpc } from '@/trpc/server';

export default async function Page(props) {
const searchParams = await props.searchParams;
const search = usersSearchParamsCache.parse(searchParams);
const validFilters = getValidFilters(search.filters);
const queryClient = getQueryClient();

const promises = Promise.all([
queryClient.fetchQuery(
trpc.providers.users.list.queryOptions({
...search,
filters: validFilters
})
)
]);

return (
<HydrateClient>
<React.Suspense fallback={<DataTableSkeleton /* props */ />}>
<UsersTable promises={promises} />
</React.Suspense>
</HydrateClient>
);
}
On the client side, I have a DeleteUsersDialog component that allows bulk deletion of users. After deletion, I invalidate the users.list query so it refetches and reflects the updated list automatically:
import { useTRPC } from '@/trpc/client';
export function DeleteUsersDialog({
users,
showTrigger = true,
onSuccess,
...props
}: UsersDialogProps) {
const trpc = useTRPC();
const queryClient = useQueryClient();

const { mutateAsync } = useMutation(
trpc.providers.users.deleteMany.mutationOptions({
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: trpc.providers.users.list.queryKey({})
});
},
onError: (error) => {
throw new Error(error.message);
}
})
);

return (
<DeleteDialog
data={users}
label="person"
showTrigger={showTrigger}
handleDelete={async (users) => {
await mutateAsync({ ids: users.map((u) => u.id) });
}}
onSuccess={onSuccess}
{...props}
/>
);
}
import { useTRPC } from '@/trpc/client';
export function DeleteUsersDialog({
users,
showTrigger = true,
onSuccess,
...props
}: UsersDialogProps) {
const trpc = useTRPC();
const queryClient = useQueryClient();

const { mutateAsync } = useMutation(
trpc.providers.users.deleteMany.mutationOptions({
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: trpc.providers.users.list.queryKey({})
});
},
onError: (error) => {
throw new Error(error.message);
}
})
);

return (
<DeleteDialog
data={users}
label="person"
showTrigger={showTrigger}
handleDelete={async (users) => {
await mutateAsync({ ids: users.map((u) => u.id) });
}}
onSuccess={onSuccess}
{...props}
/>
);
}
2 Replies
grumpy-cyan
grumpy-cyanOP•7mo ago
@TkDodo 🔮
equal-jade
equal-jade•7mo ago
Likely The key doesn't match, can't say more without a reproduction

Did you find this page helpful?