Search code examples
javascriptreactjsnext.js

React JS Stop setState batching in async function


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:

  1. Set form button to be disabled & message to "Loading".
  2. Send formData to Next.JS via an async server action.
  3. Set form button to be enabled & message to error if error.

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.


Solution

  • 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:

    • flushSync ensures that React processes and renders the state updates (SetInteractable(false) and setMessage("Loading")) before proceeding with the async operation.
    • This approach forces the UI to update immediately, so the user will see the "Loading" message and the button will be disabled before the async function starts.