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
and here is the revalidation code
Here I've tried with revalidatePath but that also does nothing
Please help!!
22 Replies
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
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
- 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-componentshi @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!!
Awesome good to know!
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)
In DeleteDialogues.tsx
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
@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?
@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.
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.
I just tested it out locally and it worked for me. It's gotta work. Are you using the same tab?
@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
@Circus Ok, I'll take a look later today
@james162861 Just a gentle reminder - did you have a chance to take a look?
@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
@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 🙏 🙏 🙏
np
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?
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
I think you can also export in the server function, so in actions.ts (or wherever), I found this works too:
And then bring in this function. This would possibly be a cleaner method than passing down
revalidatePath
?oh yea, that should work also.
This works quite well! @james162861 , is there a better way to do this now, or is this still your recommened solution
@potato I'm not sure if it is recommended. But i don't know how to make it work otherwise.
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()