TanStack / query

🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.
https://tanstack.com/query
MIT License
42.49k stars 2.91k forks source link

Next.js server action not getting executed #7934

Open RareSecond opened 2 months ago

RareSecond commented 2 months ago

Describe the bug

Please bear with me, because I have no idea how to best start. I've also done my best to narrow this down as much as possible, but I haven't been able to provide a minimal reproducible example.

Setting the stage

We were noticing more and more lately that our (Cypress) tests are flakey so I started to investigate it. I've managed to track it down to a custom hook not resolving with the correct data.

The hook, useCurrentUser, is in essence a very simple hook

import { useQuery } from "@tanstack/react-query";

import { getCurrentUserWithRoles } from "@/actions/users";

export const useCurrentUser = () => {
  const { data: currentUserWithRoles } = useQuery({
    queryFn: async () => {
      return getCurrentUserWithRoles();
    },
    queryKey: ["currentUserWithRoles"],
  });

  if (!currentUserWithRoles) {
    return null;
  }

  return currentUserWithRoles;
};

getCurrentUserWithRoles is also a very simple server action that does some database fetching.

"use server";

import { AuthDAL } from "@/data/auth";

export async function getCurrentUserWithRoles() {
  return await AuthDAL.getCurrentUserWithRoles();
}

However, when running cypress open and running a test that relies on the useCurrentUser returning correct information, the hook sometimes fails to return any data.

What I've tried

Adding logs

If I add logs to the necessary places, I can see where it goes wrong

    queryFn: async () => {
      console.log("About to get roles");
      const res = getCurrentUserWithRoles();
      console.log("res: ", res);

      return res;
    },
export async function getCurrentUserWithRoles() {
  console.log("Inside server action");
  return await AuthDAL.getCurrentUserWithRoles();
}

The first log gets printed, but the other two don't appear. So for some reason, it seems that it never reaches the server action.

Removing React Query

If I remove React Query and just rely on useEffect and useState, there's no flakiness and test passes 100% of the time

export const useCurrentUser = () => {
  const wasCalled = useRef(false);
  const [currentUserWithRoles, setCurrentUserWithRoles] = useState<Awaited<
    ReturnType<typeof getCurrentUserWithRoles>
  > | null>(null);

  useEffect(() => {
    if (!wasCalled.current) {
      wasCalled.current = true;

      getCurrentUserWithRoles().then((res) => {
        setCurrentUserWithRoles(res);
      });
    }
  }, []);

  if (!currentUserWithRoles) {
    return null;
  }

  return currentUserWithRoles;
};

Replacing the server action with an actual API call

I also went ahead and created an actual API call (via route.ts) where I returned hardcoded data. And again, it works 100% of the time.

import { NextResponse } from "next/server";

export async function POST() {
  return NextResponse.json({
    groupRoles: [
      {
        groupIdId: "77fc6325-96ff-449b-91a8-cd0a8c79e019",
        role: "ADMIN",
      },
      {
        groupId: "4b102177-be72-4910-a46d-90060ca7b178",
        role: "ADMIN",
      },
    ],
    firstName: "Fred",
    id: "33d4cde9-c366-4c77-9669-decdf0e0c6ad",
    lastName: "Flinstone",
    appRole: "ADMIN",
  });
}
export const useCurrentUser = () => {
  const { data: currentUserWithRoles } = useQuery({
    queryFn: async () => {
      // return getCurrentUserWithRoles();
      const call = await fetch("/api/v1/test", {
        method: "POST",
      });

      const res = await call.json();

      return res;
    },
    queryKey: ["currentUserWithRoles"],
  });

  if (!currentUserWithRoles) {
    return null;
  }

  return currentUserWithRoles;
};

Replacing Cypress with Playwright

I even rewrote the test in Playwright but to no avail. The tests are still just as flaky, feeling even worse (but that may be my mental state acting up by now).

Some extra insights

However, the switch to Playwright has brought some extra insights that may be helpful

Your minimal, reproducible example

I tried, but can't condense it enough to have it always fail

Steps to reproduce

/

Expected behavior

I expect React Query to actually fire the Next.js server action and return the data

How often does this bug happen?

Often

Screenshots or Videos

No response

Platform

Tanstack Query adapter

react-query

TanStack Query version

v5.52.0

TypeScript version

v5.3.3

Additional context

Please let me know what other information I can provide. I felt like I've been going down the rabbit hole, with no solution in sight..

RareSecond commented 2 months ago

Hi @neehhaa06

While it shouldn't matter, I've tried your suggestion, but nothing changed.

quintenbuis commented 2 months ago

Having the exact same issue. With me it happen when I quickly redirect to another page but useQuery started already, leaving it in a state of always saying isLoading and never updating or being able to refetch

quintenbuis commented 2 months ago

This stripped down version allowed me to continually reproduce this. (have not tested if this actually works with the stripped down code)

The component causing a redirect

const Checkout = (): ReactElement => {
    const { t } = useTranslation();
    const { cart, syncCart } = useCartStore();
    const [loading, setLoading] = useState<boolean>(true);

    useEffect(() => {
        if (cart && cart.lineItems.length === 0) {
            redirect('/cart');
        }

        setLoading(false);
    }, [cart]);

    if (loading) {
        return <></>;
    }

    return (
        <div className="container mb-10">
            <div className="flex flex-col lg:flex-row gap-8 mt-8">
                <PaymentMethods />
            </div>
        </div>
    );
};

The useQuery being used where getPaymentMethods is the server action:

const usePaymentMethods = (): UseQueryResult<PaymentMethod[]> => {
    const { shop } = useShopContext();

    return useQuery<PaymentMethod[]>({
        queryKey: ['paymentMethods', shop],
        queryFn: async () => {
            return await getPaymentMethods();
        },
    });
};

export default usePaymentMethods;

Inside PaymentMethod the useQuery is initiated due to it being rendered into the DOM, but the redirect quickly interrupts that call, causing it to be in a constant loading state.

The workaround for this is to not use server actions, but to create an API proxy yourself and there running the action. Not the most ideal fix ofcourse since it just shouldn't happen.

adamlewkowicz commented 2 months ago

I'm facing a similar issue. Few actions are called, but some are pending in an infinite isPending state, and the server action is never called. It only runs all actions then on full page refresh.

Is there going to be any fix for this, as this is a major issue?

TkDodo commented 2 months ago

Not really sure how to address this tbh. If the logs are correct and you see "About to get roles", it means the queryFn is executing and we do nothing until the queryFn finishes. If the next thing is calling the server action, then I don't see why this wouldn't happen.

That said, it's not recommended (neither from react-query side nor from nextjs side) to actually use serverACTIONS for something that aren't "actions" (things that have side-effects, we also call them mutations here).

Right now, serverActions run in serial. All of them. One by one. So if you have two unrelated queries, they will be queued up. Maybe the flakiness has to do with how cypress performs tests in parallel and they get queued, I really don't know.

I know that everyone sees the benefit of not having to write API routes because server actions are basically RPC calls but this is not what they are meant for. trpc is still pretty good for this with nextJs.

RareSecond commented 2 months ago

@TkDodo This is coming straight from the Next.js docs

Server Actions are not limited to <form> and can be invoked from event handlers, useEffect, third-party libraries, and other form elements like <button>.

This isn't a mutation in this case, since we are just fetching the data.

I want to again stress the fact that, when using a simple useEffect and (very high level, I know) recreating the behavior of react-query, everything works as expected.

I know this one's probably a massive bitch to debug, but I do think this is something with react-query that's going wrong. Would you want me to try swr and see if it suffers from the same issue?

TkDodo commented 2 months ago

This is coming straight from the Next.js docs

yes, but it's still an "action". The section in the docs is called Server Actions and Mutations, and it's next to Data Fetching and Caching. Using a server action for data fetching is, again, not recommended by next or by us.

I talked to the nextjs team not too long ago and mid-term, they want to provide something like "server actions" but for fetching. But server actions ain't it.

I want to again stress the fact that, when using a simple useEffect and (very high level, I know) recreating the behavior of react-query, everything works as expected.

I hope it's understandable that my answer here can only be:

I want to again stress the fact that, when using a simple fetch and (very high level, I know) recreating the behavior of server actions, everything works as expected.

Bottom line is I really don't even know how to look into this ...

bartcheers commented 2 months ago

Right now, serverActions run in serial. All of them. One by one. So if you have two unrelated queries, they will be queued up.

This surprised me. I tested this and it seems that it is incorrect, unless I'm missing something - server actions can run in parallel: See this Codesandbox.

TkDodo commented 2 months ago

@bartcheers your sandbox just calls an async function (that is marked as "use server") in a server component.

To really get a server action, that gets transformed to a POST request under the hood, you need to invoke the action from a client component (e.g. in a button click handler).

bartcheers commented 2 months ago

Thanks @TkDodo for clarifying. Here's another Codesandbox confirming server actions run in series, not in parallel. Great to know!

TkDodo commented 2 months ago

I think we should just amend the docs that it's not recommended to use server actions with useQuery and that API routes are still the way to go. Would someone want to do that?

RareSecond commented 2 months ago

While I'm starting to see more why we shouldn't use it for data fetching, I do still think that there's an issue on your part.

I recreated the test in Playwright and ran it 100 times (for 3 different browsers, so 300 tests in total).

This was React Query image

This was swr image

We will be making the switch to swr, as that's an easier replacement than making the switch to API routes/tRPC.

Dakuan commented 4 days ago

I've hit this as well on one of my personal projects. wierdly only happening in dev. Using server actions to fetch data client side in combination with react-query and all hell breaks loose...

  const { hasNextPage, isFetchingNextPage, fetchNextPage, data, isLoading } =
    useInfiniteQuery({
      queryKey: key,
      queryFn: ({ pageParam }) => {
        const offset = (pageParam - 1) * pageSize;
        return query(offset);
      },
      refetchOnWindowFocus: false,
      initialPageParam: 1,
      getNextPageParam: (lastPage, pages) => {
        if (lastPage.length === 0) return;
        return pages.length + 1;
      },
    });

lastPage would intermittently be null 🤷

FilipPano commented 3 days ago

I seem to be experiencing this problem in production, but with useMutation invoking a server action:

export function useUpdateProduct() {
  return useMutation({
    mutationFn: updateProduct, // this is a server action
    onSuccess: () => {
      toast.success('Your product has been saved.');
    },
    onError: (e) => {
      console.error(e);
      toast.error('Failed to save your product. Please try again.');
    },
  });
}

Once this problem occurs, this and other mutations fail without even invoking the server action. Once I refresh the page they start working again.