Why I Migrated from Server Actions to tRPC
Trying Next.js Without tRPC I recently tried to build a new Next.js app without tRPC, thinking I wouldn't need it anymore now that Server Actions are stable and there are tools like next-safe-action to help with validation, auth middleware, error handling, etc. Well... I was wrong. When It's Okay to Skip tRPC I think I could probably get away without tRPC for apps where the pages are mostly static after the initial page load. In that case, I can just read the data directly from the server component and use server actions with next-safe-action for the mutations and be happy. When tRPC Becomes Essential For apps where I have pagination, filtering, sorting, or anything that needs to fetch data after the initial page load, I can't see myself not using tRPC. tRPC (together with TanStack Query) makes it super easy to fetch the data from the server component and then be able to easily refetch it when the user interacts with the page. This not only makes the initial page load fast (as there's no initial HTTP request from the browser to fetch the data), but it also makes it super simple to implement things like pagination, filtering, sorting, etc. Here's a quick example of using prefetch with useSuspenseQuery for a smooth initial load: // app/page.tsx import { HydrateClient, prefetch, trpc } from '~/trpc/server'; import { Suspense } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { ClientGreeting } from './client-greeting'; export default function Page() { // Prefetch the query on the server without blocking the page void prefetch(trpc.hello.queryOptions({ text: 'world' })); return ( ); } // app/client-greeting.tsx 'use client'; import { trpc } from '~/trpc/client'; export function ClientGreeting() { const { data } = trpc.hello.useSuspenseQuery({ text: 'world' }); return {data.greeting}; } This gives you the benefits of fast server-side rendering and you can use TanStack Query for easily refetching when the user interacts with the page. This can be simplified even more with the component I shared on Twitter (X) that people seemed to really enjoy: https://x.com/RaviCoding/status/1912643165275975711 Why Not Server Actions + TanStack Query? For those thinking you can just use Server Actions together with useSuspenseQuery from TanStack Query, no, not really. I mean, yeah, it technically works, but if you open the console you'll see an error saying it switched to client-side rendering because Server Actions aren't meant for the initial page load. So a request will still be made from the browser to fetch the initial data. Another reason not to use Server Actions for fetching data is that they don't run in parallel. If you're fetching multiple things on the same page from different Server Actions, it'll slow down your page load a lot. If You're Already Using tRPC for Queries... Now, if I'm already using tRPC to fetch data, there's not much reason not to use it for mutations too, which basically means I end up not using Server Actions at all. What Do You Think? I'm really curious what others think. Have you found a good way of using Server Actions for these use-cases? Let me know!

Trying Next.js Without tRPC
I recently tried to build a new Next.js app without tRPC, thinking I wouldn't need it anymore now that Server Actions are stable and there are tools like next-safe-action to help with validation, auth middleware, error handling, etc.
Well... I was wrong.
When It's Okay to Skip tRPC
I think I could probably get away without tRPC for apps where the pages are mostly static after the initial page load. In that case, I can just read the data directly from the server component and use server actions with next-safe-action
for the mutations and be happy.
When tRPC Becomes Essential
For apps where I have pagination, filtering, sorting, or anything that needs to fetch data after the initial page load, I can't see myself not using tRPC.
tRPC (together with TanStack Query) makes it super easy to fetch the data from the server component and then be able to easily refetch it when the user interacts with the page. This not only makes the initial page load fast (as there's no initial HTTP request from the browser to fetch the data), but it also makes it super simple to implement things like pagination, filtering, sorting, etc.
Here's a quick example of using prefetch with useSuspenseQuery for a smooth initial load:
// app/page.tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ClientGreeting } from './client-greeting';
export default function Page() {
// Prefetch the query on the server without blocking the page
void prefetch(trpc.hello.queryOptions({ text: 'world' }));
return (
<HydrateClient>
<ErrorBoundary fallback={<div>Something went wrongdiv>}>
<Suspense fallback={<div>Loading...div>}>
<ClientGreeting />
Suspense>
ErrorBoundary>
HydrateClient>
);
}
// app/client-greeting.tsx
'use client';
import { trpc } from '~/trpc/client';
export function ClientGreeting() {
const { data } = trpc.hello.useSuspenseQuery({ text: 'world' });
return <div>{data.greeting}div>;
}
This gives you the benefits of fast server-side rendering and you can use TanStack Query for easily refetching when the user interacts with the page.
This can be simplified even more with the
component I shared on Twitter (X) that people seemed to really enjoy:
https://x.com/RaviCoding/status/1912643165275975711
Why Not Server Actions + TanStack Query?
For those thinking you can just use Server Actions together with useSuspenseQuery from TanStack Query, no, not really. I mean, yeah, it technically works, but if you open the console you'll see an error saying it switched to client-side rendering because Server Actions aren't meant for the initial page load. So a request will still be made from the browser to fetch the initial data.
Another reason not to use Server Actions for fetching data is that they don't run in parallel. If you're fetching multiple things on the same page from different Server Actions, it'll slow down your page load a lot.
If You're Already Using tRPC for Queries...
Now, if I'm already using tRPC to fetch data, there's not much reason not to use it for mutations too, which basically means I end up not using Server Actions at all.
What Do You Think?
I'm really curious what others think. Have you found a good way of using Server Actions for these use-cases? Let me know!