NextJS server action running twice leading to unexpected behavior
Answered
Rohu posted this in #help-forum
RohuOP
I have successfully reproduced this bug on a skeleton repo:
- https://github.com/dunstorm/nextjs-example-bug
I am trying to have a dynamic param
The issue is nextjs server action is running twice, after
I have tried
- Disabling
- https://github.com/dunstorm/nextjs-example-bug
I am trying to have a dynamic param
[id]
page with 2 lazy loaded components which use separate server action.The issue is nextjs server action is running twice, after
/results/loading
redirects using router.push
to /results/[id]
we call the server action multiple times, this is a very big issue since it's causing redis to increment the value by 2 instead of 1 GET /results/loading 200 in 19ms
POST /results/loading 200 in 1015ms
getSearchResults mieepjoh3ce
GET /results/mieepjoh3ce 200 in 19ms
getSearchResults mieepjoh3ce
GET /results/mieepjoh3ce 200 in 19ms
I have tried
- Disabling
reactStrictMode
Answered by LuisLl
Take a look at the latest change on my repo, only the "Generate Random Search" button is a client component, the rest are Server Components fetching data on the server, not server actions.
A component can't suspend when awaiting a Server Action to complete as the result of a mutation (clicking the button), so in this case I wrapped the click handler in
A component can't suspend when awaiting a Server Action to complete as the result of a mutation (clicking the button), so in this case I wrapped the click handler in
startTransition
to have access to the loading state and display a fallback.33 Replies
try putting the function calling redis.. inside a react
cache
wrapper@Rohu
RohuOP
Yes it's inside the
cache
wrapper but as we see here, getSearchResults
is called twice regardlessThe only solution seems like is to change to client component since I have written all of the things in nextjs now
But I would like to solve this if possible, should I raise it in github?
If you raise an issue in GitHub first have in mind that you are using Server Actions in a way they are not intended to be used. You're using them to fetch data, when it should be for mutations.
Before you raise it to GitHub try moving the data fetching logic to either a Server Component or a Route Handler. See if this solves the issue, since these are the two recommended ways for fetching data.
Before you raise it to GitHub try moving the data fetching logic to either a Server Component or a Route Handler. See if this solves the issue, since these are the two recommended ways for fetching data.
The issue here is that your page is rendering twice, therefore your
<SearchResults>
component is also rendering twice, making the call twice, in different render passes so cache
won't help here.RohuOP
Yes, that's the issue exactly!
But why is page rendering twice, I don't understand - removing suspense doesn't work either
When you call a Server Action you make a POST request to the same URL you're currently on, and Server Actions in a single round trip receive the request and respond with new React Server Component payload, not sure if this is the default bahavior or this only happens when you
revalidatePath
or revalidateTag
inside the Server Action. Anyway they'r enot meant for fetching dataRohuOP
I was intially thinking it's because of streaming but doesn't seem to be the case, even when we remove everything it still renders twice
Even when we remove server actions from the results page, it still renders twice
Even when we switch to client component it still renders twice, seems like a bug? 👀
although using the server action in the client component
useEffect
works - i.e. called onceI think I found a workaround, using
loading.tsx
in /search/loading
(for loading state) and for createSearchQuery
we use redirect
inside it and directly call the function from /search/loading
and this stops us from rendering the next page twice@Rohu I think I found a workaround, using `loading.tsx` in `/search/loading` (for loading state) and for `createSearchQuery` we use `redirect` inside it and directly call the function from `/search/loading` and this stops us from rendering the next page twice
I was dooing that, and a couple more things I'll se if it works on my end
Do you want me to push my code to a repo?
RohuOP
Yes, please! Thanks a lot for your time!
@Rohu I think I found a workaround, using `loading.tsx` in `/search/loading` (for loading state) and for `createSearchQuery` we use `redirect` inside it and directly call the function from `/search/loading` and this stops us from rendering the next page twice
RohuOP
The only problem I found with this is
loading.tsx
isn't visible for some reason@LuisLl Do you want me to push my code to a repo?
RohuOP
or I can share the repo with you, given the username
Your page needs to be a Server Component, and it needs to suspend by awaiting the async function. This way loading.tsx will render in place
Btw
So, keep your pages Server Components, mark them as async and leverage them to fetch data in there, instead of turning your page into a client component and make use of
actions.ts
became queries.ts
, no longer a Server Actions file, I removed the "use server" at the top, since Server Actions aren't supposed to be use for Data Fetching.So, keep your pages Server Components, mark them as async and leverage them to fetch data in there, instead of turning your page into a client component and make use of
useEffects
.Another thing you could do is instead of having a /loading/page.tsx that only calls the server action, make the root
page.tsx
responsible of it and redirect. But experiment with it, find better solutions and patterns👍RohuOP
Really appreciate your time!
Still not the ideal solution since the loading page doesn't even appear when the createSearchQuery is too quick and there is a delay in between the loading and the result page shown - which is displayed as a blank page
Guess I will have to convert the result page to client component 😢 or atleast the subparts
Still not the ideal solution since the loading page doesn't even appear when the createSearchQuery is too quick and there is a delay in between the loading and the result page shown - which is displayed as a blank page
Guess I will have to convert the result page to client component 😢 or atleast the subparts
It worked fine on my end, but it definitely not the ideal solution, I have to go to bed but if you haven’t solved it by tomorrow I can continue helping.
What I would do is:
- /app/page.tsx would be a Server Component and the button that starts the search would be a client component calling the Server Action. Or just wrap the button in a <form> and pass the server action to the action prop
- /app/(pages)/loading/page.tsx and /app/(pages)/loading/loading.tsx will no longer exist
- /app/(pages)/results/[id] would have a loading.tsx
What I would do is:
- /app/page.tsx would be a Server Component and the button that starts the search would be a client component calling the Server Action. Or just wrap the button in a <form> and pass the server action to the action prop
- /app/(pages)/loading/page.tsx and /app/(pages)/loading/loading.tsx will no longer exist
- /app/(pages)/results/[id] would have a loading.tsx
@Rohu Really appreciate your time!
Still not the ideal solution since the loading page doesn't even appear when the createSearchQuery is too quick and there is a delay in between the loading and the result page shown - which is displayed as a blank page
Guess I will have to convert the result page to client component 😢 or atleast the subparts
You don’t need to make them client components, if the loading state is what worries you then add a loading.tsx in /results/[id]. Try the things i mentioned I’d do, and let me know if it works
RohuOP
Yes, I have tried the above - the thing I am trying to achieve here is to remove the delay created by
So when I press the button, it's a blocking operation and it doesn't feel as smooth as the skeleton loading page to the user so that's why the intermediatery page
THe
createSearchQuery
So when I press the button, it's a blocking operation and it doesn't feel as smooth as the skeleton loading page to the user so that's why the intermediatery page
THe
loading.tsx
works for solving the twice render problem but NextJS is pretty weird, it sometimes doesn't show the loading.tsx
and if it does, it stops showing that once createSearchQuery
is complete and shows just a blank page while redirectingThe blank page problem doesn't occur if we are using client component and calling
I guess the only solution is to make everything a client component - it doesn't matter for this use case because results page won't be scraped by search engines
createSearchQuery
in useEffect
which I think makes sense but then the twice render problemI guess the only solution is to make everything a client component - it doesn't matter for this use case because results page won't be scraped by search engines
I will try to reproduce the blank page issue in the repo! Thanks for everything you have done so far!
@Rohu I have successfully reproduced this bug on a skeleton repo:
- https://github.com/dunstorm/nextjs-example-bug
I am trying to have a dynamic param `[id]` page with 2 lazy loaded components which use separate server action.
The issue is nextjs server action is running twice, after `/results/loading` redirects using `router.push` to `/results/[id]` we call the server action multiple times, this is a very big issue since it's causing redis to increment the value by 2 instead of 1
GET /results/loading 200 in 19ms
POST /results/loading 200 in 1015ms
getSearchResults mieepjoh3ce
GET /results/mieepjoh3ce 200 in 19ms
getSearchResults mieepjoh3ce
GET /results/mieepjoh3ce 200 in 19ms
I have tried
- Disabling `reactStrictMode`
we call the server action multiple times,Umm, so you are running it multiple times, so I guess it'll increment twice
oh wait nvm the issue seems to be resolved as per the latest messages
@Rohu The blank page problem doesn't occur if we are using client component and calling `createSearchQuery` in `useEffect` which I think makes sense but then the twice render problem
I guess the only solution is to make everything a client component - it doesn't matter for this use case because results page won't be scraped by search engines
Take a look at the latest change on my repo, only the "Generate Random Search" button is a client component, the rest are Server Components fetching data on the server, not server actions.
A component can't suspend when awaiting a Server Action to complete as the result of a mutation (clicking the button), so in this case I wrapped the click handler in
A component can't suspend when awaiting a Server Action to complete as the result of a mutation (clicking the button), so in this case I wrapped the click handler in
startTransition
to have access to the loading state and display a fallback.Answer