I have the react client component, it's a part of a next.js app, this component is a simple form that sends it's formData to a next.js server action through an async function that will do the following steps:
The problem seems to be that react is batching my setState calls, so in execution it's as if step 1 doesn't even happen, just step 2 + 3.
I can't figure out how to circumvent this behaviour.
Here's the source code:
"use client";
import { SendGiftAction } from "@/app/actions/SendGiftAction";
import { useState } from "react";
export default function GoldGiftForm()
{
let [interactable, SetInteractable] = useState(true);
let [message, setMessage] = useState("");
async function Execute(data: FormData)
{
//These won't change the UI
SetInteractable(false);
setMessage("Loading");
//-------------------------
//Next.JS Server action
let response = await SendGiftAction(data);
//These will change the UI
SetInteractable(true);
setMessage(response.Success ? "" : response.Error);
//------------------------
}
return (
<form action={Execute}>
<h3>Send Gold Gift</h3>
<input
name="userID"
type="text"
placeholder="User ID"
/>
<input
name="gold"
type="number"
placeholder="Gold"
/>
<button type="submit" disabled={!interactable}> Send Gift </button>
<p>{message}</p>
</form>
);
}
This is a code snippet for SendGiftAction: https://pastebin.com/YYQeG0m4
Which is a wrapper that validates data and invokes SendSystemGift: https://pastebin.com/uD0LLEFL
Both are standard async functions really.
The issue you're encountering arises from the fact that React batches state updates inside asynchronous functions like await or promises. So, when you call SetInteractable(false) and setMessage("Loading"), React delays rendering until the await SendGiftAction(data) finishes, which makes it seem like those UI updates don't happen before the async call completes.
To address this, you can force React to apply the first set of state changes before executing the await by flushing the state updates. One way to do this is to use flushSync from react-dom, which ensures that React immediately processes the state updates before continuing with the async logic.
Solution: Use flushSync to force immediate state updates Here's how you can modify your Execute function:
"use client";
import { SendGiftAction } from "@/app/actions/SendGiftAction";
import { useState } from "react";
import { flushSync } from "react-dom";
export default function GoldGiftForm() {
let [interactable, SetInteractable] = useState(true);
let [message, setMessage] = useState("");
async function Execute(data: FormData) {
// Force React to update the state immediately
flushSync(() => {
SetInteractable(false);
setMessage("Loading");
});
// Next.JS Server action
let response = await SendGiftAction(data);
// Update UI after server action completes
SetInteractable(true);
setMessage(response.Success ? "" : response.Error);
}
return (
<form action={Execute}>
<h3>Send Gold Gift</h3>
<input
name="userID"
type="text"
placeholder="User ID"
/>
<input
name="gold"
type="number"
placeholder="Gold"
/>
<button type="submit" disabled={!interactable}> Send Gift </button>
<p>{message}</p>
</form>
);
}
Explanation: