Query Invalidation with trpc not working with React Server Components and App Router

Hi guys, I am trying to do Optimistic updates with trpc but the problem is I am fetching data on the server component using trpc and passing it to client component
const Dashboard = async () => {
noStore();
const session = await getServerAuthSession();

if (!session) {
redirect("/");
}

const dbWallets = await api.wallet.getAll();

return (
const Dashboard = async () => {
noStore();
const session = await getServerAuthSession();

if (!session) {
redirect("/");
}

const dbWallets = await api.wallet.getAll();

return (
and here is the revalidation code
const { mutate, isPending } = api.wallet.create.useMutation({
onSuccess: () => {
toast("Wallet has been created");
void utils.wallet.getAll.invalidate();
setIsOpen(false);
//revalidatePath("/dashboard");
},
});
const { mutate, isPending } = api.wallet.create.useMutation({
onSuccess: () => {
toast("Wallet has been created");
void utils.wallet.getAll.invalidate();
setIsOpen(false);
//revalidatePath("/dashboard");
},
});
Here I've tried with revalidatePath but that also does nothing Please help!!
22 Replies
aditya
adityaOP8mo ago
I have deployed the application if anyone wants to try and see whats happening github: https://github.com/aditya-exe/money-mogul deployed: https://money-mogul.vercel.app
GitHub
GitHub - aditya-exe/money-mogul: A expense tracker for me
A expense tracker for me. Contribute to aditya-exe/money-mogul development by creating an account on GitHub.
Money Mogul
Created by a1000
Max
Max8mo ago
I think cache invalidation from the client side doesn't automatically trigger a re-invocation of the call from the server side. Observers are created via useQuery so on the server, you have none. So if you'd expect invalidateQueries to refetch the queries, this wouldn't happen. Possible Solutions I found, haven’t done this myself yet.
- refetch instead of invalidate? - I dont think it’s that simple tho - revalidatePath - This is what you want I think - https://nextjs.org/docs/app/api-reference/functions/revalidatePath - router.refresh() - I think this would also work - https://nextjs.org/docs/app/building-your-application/caching#routerrefresh let me know if any of those work or if you find a better solution. I'm seeing router.refresh() used more. You might be better off just using using suspense. "use client" components are deceptive as they do still run on the server. If you wrap it with a suspense you'll have a faster intial paint with your layout then it will stream in your component that relies on data when the data is available. So then you would be able to just invalidate it like you're trying to. U would just need to call const dbWallets = await api.wallet.getAll(); using the client api and wrap it with a suspense. https://www.joshwcomeau.com/react/server-components/ https://nextjs.org/docs/app/building-your-application/rendering/server-components
aditya
adityaOP8mo ago
hi @Max thanks for useful info, right now router.refresh() is working like expected i guess "revalidatePath only invalidates the cache when the included path is next visited. This means calling revalidatePath with a dynamic route segment will not immediately trigger many revalidations at once. The invalidation only happens when the path is next visited." this is why revalidatePath is not working i will try doing this too but for now router works thanks!!
Max
Max8mo ago
Awesome good to know!
james162861
james1628618mo ago
I was running into a similar issue and I found the solution. I have a server component that listed dialogues, and then part of that listing was a client component called DeleteDialogue that I used to delete the dialogues on click. I wanted the dialogue listing cache to invalidate when i deleted a dialogue- really just refetch the new list of entities. I think that invalidating the cache does not work because the listing page is a server component. Therefore, I needed to pass down a callback from the listing page to the client DeleteButton so that it revalidates the path. In DialoguesList.tsx (server comp)
const handleDeleteCallback = async () => {
"use server";
revalidatePath("/dialogues");
};
...

<DeleteDialogue
handleDeleteCallback={handleDeleteCallback}
id={d.id}
/>
const handleDeleteCallback = async () => {
"use server";
revalidatePath("/dialogues");
};
...

<DeleteDialogue
handleDeleteCallback={handleDeleteCallback}
id={d.id}
/>
In DeleteDialogues.tsx
const deleteDialogue = api.dialogue.delete.useMutation({
onSuccess: () => {
// Refresh the list of dialogues
console.log("Deleted dialogue");
handleDeleteCallback();
},
});
const deleteDialogue = api.dialogue.delete.useMutation({
onSuccess: () => {
// Refresh the list of dialogues
console.log("Deleted dialogue");
handleDeleteCallback();
},
});
I think you need to pass down a callback from the server component to the client component that will revalidate the path as explained here https://discord.com/channels/966627436387266600/1224762014010970252/1229995324463124490
Circus
Circus8mo ago
@james162861 this process would only work for a child component existing on the same url path, right? Is there a way to handle this revalidation for non-child components and/or different paths?
james162861
james1628618mo ago
@Circus 1) Different Paths: Let's say you are on /settings and want to delete a dialogue that is listed on /dialogues. When you make that deletion and then navigate to /dialogues, the data will be refetched and the dialogue will no longer show so i dont think you need to use revalidatePath. This is only useful when you have a everything in the same view- same path. 2) Parent components: The above example was <SeverComp><ClientComponent/></ServerComponent>, where the server is listing the dialogues and the client is deleting. If the structure was reversed, <ClientComponent><ServerComponent/></ClientComponent>, you would have lift the funciton up into the parent Client component. I dont this is a good idea.
Circus
Circus8mo ago
I can confirm #2, but #1 does not work in TRPC, which is the issue I've encountered. The queries in server components seem to be cached in a way that navigating back to a page that has already been rendered will hold onto the old data until you refresh the page.
james162861
james1628618mo ago
I just tested it out locally and it worked for me. It's gotta work. Are you using the same tab?
Circus
Circus8mo ago
@james162861 Edited for clarification: I have attached a minimum repo link showing what I mean with a simple readme using T3's set up post router to show the issue. No packages added, using T3 stack @latest and minimal changes https://github.com/spenserschwartz/t3-trpc-revalidate-issue
james162861
james1628618mo ago
@Circus Ok, I'll take a look later today
Circus
Circus8mo ago
@james162861 Just a gentle reminder - did you have a chance to take a look?
james162861
james1628618mo ago
@Circus ok fixed. You had the page.tsx as a client component directly. I dont think this is best practice (could be wrong). But by making the page.tsx a server component, you can pass down the revalidate function to your client component. You can also use the refetch() function on the client side to update the client component. Hope this helps https://github.com/spenserschwartz/t3-trpc-revalidate-issue/pull/1/files
Circus
Circus8mo ago
@james162861 You just scratched an itch I've had for so long. Dude, thank you so much! This is exactly what I've been looking for 🙏 🙏 🙏
james162861
james1628618mo ago
np
Circus
Circus8mo ago
Actually, I have a followup. Is there a way to have the api fetch in the server component that is disseminated to the client components? The screenshot is what we currently have, but say there were several client components being rendered in DetailPage. As it currently stands, each client component would be making an api call via hooks instead of having one source of truth. What would you do then?
No description
james162861
james1628618mo ago
you can pass down the data as props or use a context. I dont think there is a way to have a centralized source directly using react query. maybe you can do something funky with the cache, but i am not sure
Circus
Circus8mo ago
I think you can also export in the server function, so in actions.ts (or wherever), I found this works too:
"use server";
export const revalidate = async () => {
revalidatePath("/all-posts");
}
"use server";
export const revalidate = async () => {
revalidatePath("/all-posts");
}
And then bring in this function. This would possibly be a cleaner method than passing down revalidatePath?
james162861
james1628618mo ago
oh yea, that should work also.
potato
potato4mo ago
This works quite well! @james162861 , is there a better way to do this now, or is this still your recommened solution
james162861
james1628614mo ago
@potato I'm not sure if it is recommended. But i don't know how to make it work otherwise.
Ahmed Senousy
Ahmed Senousy4mo ago
I guess if you want a "best practice" it would be: 1. page.tsx should be a server component 2. you call void api.whatever.whatever.prefetch() 3. you make a client component for displaying your content and wrap it with a <HydrateClient> which is the t3 custom <HydrationBoundary> 4. inside the client component you use <Suspense> for the parts you want to get streamed in from the server and you call const [data] = await api.whatever.whatever.useSuspenseQuery()
Want results from more Discord servers?
Add your server