React Query with Server Actions
9 Mar 2025
With the release of Server Components and then Server Actions in Next.js, it appeared as if React Query was going to become a thing of the past.
Server Components
Server components allow you to fetch data on the server and then send it to the client. This would remove the need for the old way of doing client side fetches with React Query with useQuery
.
This pattern has been amazing for performance as it allows you to fetch data on the server which is usually closer to the database and then send it to the client. This is a huge improvement compared to sending an empty page and then doing another round trip to fetch the data. Conceptually it's also quite nice to work with.
While this is great for the initial page load, often you will want to fetch more data or change parameters after the page has loaded. The general pattern here would be to just refetch the entire page with the new data. This is fine for most cases but certainly isn't necessary.
This is where React Query can be used to complement Server Components. Here we have a server component that makes use of prefetchQuery
to fetch the data on the server.
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query'
import Posts from '@/components/posts'
import { getPosts } from '@/actions/posts'
export default async function PostsPage() {
const queryClient = new QueryClient()
const { tag } = await searchParams;
await queryClient.prefetchQuery({
queryKey: ['posts', tag],
queryFn: () => getPosts(tag),
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Posts />
</HydrationBoundary>
)
}
Now you can have a regular useQuery
hook in your Posts component that will use the data that was prefetched on the server. Then if the user wants to change the tags you can use the same useQuery
hook to fetch the new data and update the UI without having to refetch the entire page. This feels like a best of both worlds scenario.
Note that getPosts
used here is actually a function that doubles as a server action! Which we will make use of later.
Server Actions
Server Actions would allow you to create server side functions that could be called from the client side. This would remove the need for useMutation
.
Unfortunately, the current implentation of Server Actions feels quite clunky.
A relatibely common pattern I have used is to fire a mutation and then fire a toast if the mutation was successful (or unsuccessful). This has always been quite simple with React Query, by using the onSuccess
callback.
The pattern for acheiving this just with Server Actions would look something like this:
'use client'
import { useActionState } from 'react';
import { createUser } from '@/actions/user';
import { toast } from '@/components';
const initialState = {
message: '',
};
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState);
useEffect(() => {
if (state?.data) {
toast.success('User created');
}
}, [state?.data]);
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
<button disabled={pending}>Sign up</button>
</form>
)
}
I may just not be used to this pattern yet, but it feels awful. useEffect
is the devil and should almost never be used if it can be avoided.useActionState
feels like a significant step backwards from useMutation
.
What server actions do enable is the ability to write functions that can be called from the client side. This is an idea conceptually the same as trpc. This concept was perfect as it enables you typesafe functions across the client and server boundary.
From here it is an easy jump to using Server Actions from within React Query itself! Using the posts example from earlier we can create a Server Action to add a new post. The example uses drizzle ORM but you can use any database library or external fetch call depending on where you are fetching from.
"use server";
import { posts } from "@/db/schema";
import { db } from "@/db";
import { getAnonymousId } from "@/lib/user";
export const addPost = async (content: string, tag: string) => {
await db.insert(posts).values({
tag: tag,
content: content,
});
};
export const getPosts = async (tag: string) => {
return await db
.select({
count: count(),
})
.from(posts)
.where(eq(posts.tag, tag));
};
And then use the Server Action in a regular React Query hook.
import { getPosts, addPost } from "@/actions/posts";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function usePosts(tag: string) {
return useQuery({
queryKey: ["posts", tag],
queryFn: () => getPosts(tag),
});
}
export function useAddPost() {
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["posts"],
mutationFn: (content: string, tag: string) => addPost(content, tag),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["posts"],
});
},
});
}
There you have all the benefits of React Query with the simplicity of Server Actions!
React has never been responsible for fetching data, and React Query is one of the best tools for that job. Server Components and Server Actions are great new tools within the React ecosystem, but they do not need to replace React Query.
Client Components
Another trend I have noticed from the Next.js mindset is that client components are seen as being a bad thing. I think this simply isn't true. There is a time and a place for client components; they are just another weapon in the toolkit.
For example on this very post I use a client component for the likes and comments. The rest of the page is statically generated as that is the most performant way to do it. But the likes and comments are fetched with a useQuery
hook that calls a Server Action to populate the data that it needs.
This all happens on the client side because in this case I don't want to have any server side logic running as that would opt-out of static rendering and slow down the page load for the content that matters.
Conclusion
Server Components and Server Actions are great new tools that have been added to the Next.js ecosystem. They are not a replacement for React Query, but rather a complement to it.
Other frameworks such as Astro are also shaking things up with new ways of thinking such as Server Islands. I have no doubt that TanStack Start will also be one to watch in this space. It will be interesting to see how these new paradigms develop and feed back into the React ecosystem.
We will see if the Next.js or React team develops features such as useActionState
further into something that is more usable. But for now, I think React Query is still the best tool for fetching data in React.
Loading...