DEV Community

Cover image for React Query and Server Actions Don't Mix: How API Routes Saved the Day
Talisson
Talisson

Posted on

2

React Query and Server Actions Don't Mix: How API Routes Saved the Day

While working on a feature that fetched data using React Query in a Next.js App Router project, I came across an issue where some queries were stuck in the loading state indefinitely. After a bit of digging, I discovered that the root cause was related to how server actions behave in Next.js β€” and how they can’t be called directly from the client.

In this post, I’ll explain the problem, how I investigated it, and how Next.js API Routes helped solve it.


🧹 The Problem

We were using useQuery from React Query in a client component to fetch data:

export const useGetCommentsQuery = (interviewId: string) =>
  useQuery({
    queryKey: ["interviewComments", interviewId],
    queryFn: () => getInterviewComments(interviewId), // ❌ server action
    enabled: !!interviewId,
  });
Enter fullscreen mode Exit fullscreen mode

The function getInterviewComments is a server action β€” a function that runs on the server and can access the database (usually via Prisma or internal logic).

However, because this code is running inside a client component, and useQuery is executed in the browser, we were effectively trying to call a server-only function from the client, which doesn’t work in Next.js.


πŸ› Symptoms

  • The hook stayed stuck in loading.
  • No data was returned.
  • No errors were thrown.
  • Console logs inside the server action were never triggered.

πŸ”Ž Why It Happens (Next.js Context)

In Next.js (with the App Router):

  • Server actions can only be called from the server (e.g., in a Server Component or via form submissions).
  • When you try to call them directly from the client (like inside a useQuery hook), they are not executed β€” and the Promise never resolves.
  • This leads to React Query hooks staying stuck in loading forever.

This isn't a bug in React Query β€” it's simply a misuse of where server-only code is allowed to run.

Here’s a visual of what I was (wrongly) attempting:

![Incorrect: Client trying to call server action directly(https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y2gzar0p6gb61ooatkfd.png)


βœ… The Solution: Use API Routes

To solve the problem, I used Next.js API Routes (via app/api) to expose server-side logic in a way that the client could consume safely using fetch.

Step 1: Create an API route

// app/api/interviews/[id]/comments/route.ts

import { NextResponse } from "next/server";
import { getInterviewComments } from "@/app/(admin)/exams/_actions/comments/get-interview-comments";

export async function GET(
  req: Request,
  { params }: { params: { id: string } }
) {
  try {
    const comments = await getInterviewComments(params.id);
    return NextResponse.json(comments);
  } catch (error) {
    console.error("Failed to fetch comments:", error);
    return new NextResponse("Failed to fetch comments", { status: 500 });
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Update the React Query hook to use fetch

export const useGetCommentsQuery = (interviewId: string) =>
  useQuery({
    queryKey: ["interviewComments", interviewId],
    queryFn: async () => {
      const res = await fetch(`/api/interviews/${interviewId}/comments`);
      if (!res.ok) throw new Error("Failed to fetch comments");
      return res.json();
    },
    enabled: !!interviewId,
  });
Enter fullscreen mode Exit fullscreen mode

Here’s what the correct architecture looks like now:

Correct: Client -> API Route -> Server Action


πŸš€ Outcome

  • The useQuery hook now resolves correctly.
  • Loading state behaves as expected.
  • Server-only logic stays on the server.
  • The API route acts as a clean and secure middle layer.

🧐 Key Takeaways

  • In Next.js App Router, server actions cannot be called directly from the client.
  • React Query expects a queryFn that works in the browser β€” server actions do not meet that requirement.
  • To bridge the gap, use API Routes (app/api) to expose server logic in a way that client components can call via fetch.

If you’re using React Query and seeing loading states that never resolve, double-check if your queryFn is trying to call a server-only function directly. If it is β€” create a proper API route to handle it.

Thanks for reading! Let me know if you’ve run into a similar situation or have better patterns to share.

Top comments (0)

Neon image

Next.js applications: Set up a Neon project in seconds

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Get started β†’